@hyperbridge/sdk 2.8.0 → 2.8.3

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.
@@ -2041,6 +2041,12 @@ var CryptoUtils = class _CryptoUtils {
2041
2041
  * signing infrastructure (hardware wallets, MPC/TEE policy engines) instead
2042
2042
  * of an opaque 32-byte digest.
2043
2043
  *
2044
+ * The payload must be a standard self-describing `eth_signTypedData_v4`
2045
+ * payload — `EIP712Domain` listed in `types`, `chainId` as a JSON number —
2046
+ * because some signing backends (e.g. MPC Vault) hash it server-side from
2047
+ * the JSON rather than locally via viem. viem ignores both details when
2048
+ * hashing, so the digest is unchanged for local signers.
2049
+ *
2044
2050
  * @param userOp - The packed UserOperation to sign (signature field ignored).
2045
2051
  * @param entryPoint - Address of the EntryPoint v0.8 contract.
2046
2052
  * @param chainId - Chain ID of the network on which the operation will execute.
@@ -2051,10 +2057,22 @@ var CryptoUtils = class _CryptoUtils {
2051
2057
  domain: {
2052
2058
  name: "ERC4337",
2053
2059
  version: "1",
2054
- chainId,
2060
+ // Runtime number so JSON.stringify emits a canonical v4 numeric chainId for
2061
+ // server-side hashers; viem's uint256 type mapping wants bigint but its
2062
+ // runtime accepts numbers, hence the cast.
2063
+ chainId: Number(chainId),
2055
2064
  verifyingContract: entryPoint
2056
2065
  },
2066
+ // `as const`: viem derives the domain's TYPE from `types.EIP712Domain`, so the
2067
+ // entries must stay string literals — widened `string` fields make viem's
2068
+ // typed-data generics reject the payload at every call site.
2057
2069
  types: {
2070
+ EIP712Domain: [
2071
+ { name: "name", type: "string" },
2072
+ { name: "version", type: "string" },
2073
+ { name: "chainId", type: "uint256" },
2074
+ { name: "verifyingContract", type: "address" }
2075
+ ],
2058
2076
  PackedUserOperation: [
2059
2077
  { name: "sender", type: "address" },
2060
2078
  { name: "nonce", type: "uint256" },
@@ -2318,6 +2336,107 @@ var CryptoUtils = class _CryptoUtils {
2318
2336
  }
2319
2337
  };
2320
2338
 
2339
+ // src/protocols/intents/uniswap-v4-position.ts
2340
+ var Q96 = 1n << 96n;
2341
+ var MIN_TICK = -887272;
2342
+ var MAX_TICK = 887272;
2343
+ function word(data, index) {
2344
+ const hex = data.startsWith("0x") ? data.slice(2) : data;
2345
+ const start = index * 64;
2346
+ if (hex.length < start + 64) throw new Error(`return data too short for word ${index}`);
2347
+ return BigInt(`0x${hex.slice(start, start + 64)}`);
2348
+ }
2349
+ function asSigned(value, bits) {
2350
+ const width = 1n << BigInt(bits);
2351
+ const masked = value & width - 1n;
2352
+ return masked >= width >> 1n ? masked - width : masked;
2353
+ }
2354
+ function addressFromWord(data, index) {
2355
+ const hex = data.startsWith("0x") ? data.slice(2) : data;
2356
+ return `0x${hex.slice(index * 64 + 24, (index + 1) * 64)}`.toLowerCase();
2357
+ }
2358
+ function getSqrtRatioAtTick(tick) {
2359
+ if (!Number.isInteger(tick) || tick < MIN_TICK || tick > MAX_TICK) {
2360
+ throw new Error(`tick out of range: ${tick}`);
2361
+ }
2362
+ const absTick = BigInt(Math.abs(tick));
2363
+ let ratio = (absTick & 0x1n) !== 0n ? 0xfffcb933bd6fad37aa2d162d1a594001n : 0x100000000000000000000000000000000n;
2364
+ const factors = [
2365
+ [0x2n, 0xfff97272373d413259a46990580e213an],
2366
+ [0x4n, 0xfff2e50f5f656932ef12357cf3c7fdccn],
2367
+ [0x8n, 0xffe5caca7e10e4e61c3624eaa0941cd0n],
2368
+ [0x10n, 0xffcb9843d60f6159c9db58835c926644n],
2369
+ [0x20n, 0xff973b41fa98c081472e6896dfb254c0n],
2370
+ [0x40n, 0xff2ea16466c96a3843ec78b326b52861n],
2371
+ [0x80n, 0xfe5dee046a99a2a811c461f1969c3053n],
2372
+ [0x100n, 0xfcbe86c7900a88aedcffc83b479aa3a4n],
2373
+ [0x200n, 0xf987a7253ac413176f2b074cf7815e54n],
2374
+ [0x400n, 0xf3392b0822b70005940c7a398e4b70f3n],
2375
+ [0x800n, 0xe7159475a2c29b7443b29c7fa6e889d9n],
2376
+ [0x1000n, 0xd097f3bdfd2022b8845ad8f792aa5825n],
2377
+ [0x2000n, 0xa9f746462d870fdf8a65dc1f90e061e5n],
2378
+ [0x4000n, 0x70d869a156d2a1b890bb3df62baf32f7n],
2379
+ [0x8000n, 0x31be135f97d08fd981231505542fcfa6n],
2380
+ [0x10000n, 0x9aa508b5b7a84e1c677de54f3e99bc9n],
2381
+ [0x20000n, 0x5d6af8dedb81196699c329225ee604n],
2382
+ [0x40000n, 0x2216e584f5fa1ea926041bedfe98n],
2383
+ [0x80000n, 0x48a170391f7dc42444e8fa2n]
2384
+ ];
2385
+ for (const [bit, factor] of factors) {
2386
+ if ((absTick & bit) !== 0n) ratio = ratio * factor >> 128n;
2387
+ }
2388
+ if (tick > 0) ratio = (1n << 256n) / ratio;
2389
+ return (ratio >> 32n) + (ratio % (1n << 32n) === 0n ? 0n : 1n);
2390
+ }
2391
+ function positionTicks(info) {
2392
+ return {
2393
+ tickLower: Number(asSigned(info >> 8n, 24)),
2394
+ tickUpper: Number(asSigned(info >> 32n, 24))
2395
+ };
2396
+ }
2397
+ function getAmountsForLiquidity(params) {
2398
+ let { sqrtRatioAX96, sqrtRatioBX96 } = params;
2399
+ const { sqrtPriceX96, liquidity } = params;
2400
+ if (sqrtRatioAX96 > sqrtRatioBX96) [sqrtRatioAX96, sqrtRatioBX96] = [sqrtRatioBX96, sqrtRatioAX96];
2401
+ const amount0For = (from, to) => liquidity * Q96 * (to - from) / to / from;
2402
+ const amount1For = (from, to) => liquidity * (to - from) / Q96;
2403
+ if (sqrtPriceX96 <= sqrtRatioAX96) {
2404
+ return { amount0: amount0For(sqrtRatioAX96, sqrtRatioBX96), amount1: 0n };
2405
+ }
2406
+ if (sqrtPriceX96 < sqrtRatioBX96) {
2407
+ return {
2408
+ amount0: amount0For(sqrtPriceX96, sqrtRatioBX96),
2409
+ amount1: amount1For(sqrtRatioAX96, sqrtPriceX96)
2410
+ };
2411
+ }
2412
+ return { amount0: 0n, amount1: amount1For(sqrtRatioAX96, sqrtRatioBX96) };
2413
+ }
2414
+ function decodePoolAndPositionInfo(data) {
2415
+ const hex = data.startsWith("0x") ? data.slice(2) : data;
2416
+ if (hex.length < 6 * 64) throw new Error("getPoolAndPositionInfo returned too little data");
2417
+ const { tickLower, tickUpper } = positionTicks(word(data, 5));
2418
+ return {
2419
+ currency0: addressFromWord(data, 0),
2420
+ currency1: addressFromWord(data, 1),
2421
+ poolKeyEncoded: `0x${hex.slice(0, 5 * 64)}`,
2422
+ tickLower,
2423
+ tickUpper
2424
+ };
2425
+ }
2426
+ function positionAmountOfToken(params) {
2427
+ const { info, liquidity, sqrtPriceX96 } = params;
2428
+ const outputToken = params.outputToken.toLowerCase();
2429
+ if (outputToken !== info.currency0 && outputToken !== info.currency1) return 0n;
2430
+ if (liquidity <= 0n || sqrtPriceX96 <= 0n) return 0n;
2431
+ const { amount0, amount1 } = getAmountsForLiquidity({
2432
+ sqrtPriceX96,
2433
+ sqrtRatioAX96: getSqrtRatioAtTick(info.tickLower),
2434
+ sqrtRatioBX96: getSqrtRatioAtTick(info.tickUpper),
2435
+ liquidity
2436
+ });
2437
+ return outputToken === info.currency0 ? amount0 : amount1;
2438
+ }
2439
+
2321
2440
  // src/protocols/intents/phantom-aggregation.ts
2322
2441
  var ENTRY_POINT_V08_ADDRESS = "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108";
2323
2442
  var injectedFetch;
@@ -2331,6 +2450,14 @@ function rpcFetch() {
2331
2450
  }
2332
2451
  return f;
2333
2452
  }
2453
+ var PhantomRpcError = class extends Error {
2454
+ constructor(message, cause) {
2455
+ super(message);
2456
+ this.cause = cause;
2457
+ this.name = "PhantomRpcError";
2458
+ }
2459
+ cause;
2460
+ };
2334
2461
  async function rpcCall(url, payload) {
2335
2462
  let lastErr;
2336
2463
  for (let attempt = 0; attempt < 4; attempt++) {
@@ -2348,23 +2475,46 @@ async function rpcCall(url, payload) {
2348
2475
  }),
2349
2476
  timeout
2350
2477
  ]);
2351
- return await response.json();
2478
+ const body = await response.json();
2479
+ if (body?.error) {
2480
+ lastErr = new Error(`rpc error: ${JSON.stringify(body.error).slice(0, 200)}`);
2481
+ continue;
2482
+ }
2483
+ return body;
2352
2484
  } catch (err) {
2353
2485
  lastErr = err;
2354
2486
  } finally {
2355
2487
  if (timer) clearTimeout(timer);
2356
2488
  }
2357
2489
  }
2358
- throw lastErr;
2490
+ throw new PhantomRpcError(`RPC call failed after 4 attempts: ${url}`, lastErr);
2359
2491
  }
2360
2492
  var FILL_ORDER_ABI = IntentGatewayV2_default.ABI;
2361
- var DECLARATION_VERSION = 1;
2362
- var MAX_DECLARED_CHAINS = 255;
2363
- function encodeAcceptedSourceChains(chains) {
2364
- if (chains.length > MAX_DECLARED_CHAINS) {
2365
- throw new Error(`Cannot declare more than ${MAX_DECLARED_CHAINS} source chains`);
2493
+ var DECLARATION_V1 = 1;
2494
+ var DECLARATION_V2 = 2;
2495
+ var MAX_DECLARED_ENTRIES = 255;
2496
+ var MAX_TOKEN_ID_BYTES = 32;
2497
+ function tokenIdToBytes(tokenId) {
2498
+ if (tokenId < 0n) throw new Error(`Uniswap V4 tokenId cannot be negative: ${tokenId}`);
2499
+ const bytes = [];
2500
+ let rest = tokenId;
2501
+ while (rest > 0n) {
2502
+ bytes.unshift(Number(rest & 0xffn));
2503
+ rest >>= 8n;
2366
2504
  }
2367
- const bytes = [DECLARATION_VERSION, chains.length];
2505
+ return bytes.length > 0 ? bytes : [0];
2506
+ }
2507
+ function encodePhantomBidDeclaration(declaration) {
2508
+ const chains = declaration.acceptedSourceChains ?? [];
2509
+ const positions = declaration.uniswapV4Positions ?? [];
2510
+ if (chains.length > MAX_DECLARED_ENTRIES) {
2511
+ throw new Error(`Cannot declare more than ${MAX_DECLARED_ENTRIES} source chains`);
2512
+ }
2513
+ if (positions.length > MAX_DECLARED_ENTRIES) {
2514
+ throw new Error(`Cannot declare more than ${MAX_DECLARED_ENTRIES} Uniswap V4 positions`);
2515
+ }
2516
+ const version = positions.length > 0 ? DECLARATION_V2 : DECLARATION_V1;
2517
+ const bytes = [version, chains.length];
2368
2518
  for (const chain of chains) {
2369
2519
  const encoded = util.stringToU8a(chain);
2370
2520
  if (encoded.length === 0 || encoded.length > 255) {
@@ -2372,25 +2522,59 @@ function encodeAcceptedSourceChains(chains) {
2372
2522
  }
2373
2523
  bytes.push(encoded.length, ...encoded);
2374
2524
  }
2525
+ if (version === DECLARATION_V2) {
2526
+ bytes.push(positions.length);
2527
+ for (const tokenId of positions) {
2528
+ const encoded = tokenIdToBytes(tokenId);
2529
+ if (encoded.length > MAX_TOKEN_ID_BYTES) {
2530
+ throw new Error(`Uniswap V4 tokenId exceeds uint256: ${tokenId}`);
2531
+ }
2532
+ bytes.push(encoded.length, ...encoded);
2533
+ }
2534
+ }
2375
2535
  return util.u8aToHex(new Uint8Array(bytes));
2376
2536
  }
2377
- function decodeAcceptedSourceChains(paymasterAndData) {
2378
- if (!paymasterAndData || !util.isHex(paymasterAndData)) return null;
2537
+ function decodePhantomBidDeclaration(paymasterAndData) {
2538
+ const absent = { acceptedSources: null, uniswapV4Positions: [] };
2539
+ if (!paymasterAndData || !util.isHex(paymasterAndData)) return absent;
2379
2540
  const bytes = util.hexToU8a(paymasterAndData);
2380
- if (bytes.length < 2 || bytes[0] !== DECLARATION_VERSION) return null;
2381
- const count = bytes[1];
2541
+ if (bytes.length < 2) return absent;
2542
+ const version = bytes[0];
2543
+ if (version !== DECLARATION_V1 && version !== DECLARATION_V2) return absent;
2382
2544
  const chains = [];
2383
2545
  let offset = 2;
2384
- for (let entry = 0; entry < count; entry++) {
2385
- if (offset >= bytes.length) return null;
2546
+ for (let entry = 0; entry < bytes[1]; entry++) {
2547
+ if (offset >= bytes.length) return absent;
2386
2548
  const length = bytes[offset];
2387
2549
  offset += 1;
2388
- if (length === 0 || offset + length > bytes.length) return null;
2550
+ if (length === 0 || offset + length > bytes.length) return absent;
2389
2551
  chains.push(util.u8aToString(bytes.subarray(offset, offset + length)));
2390
2552
  offset += length;
2391
2553
  }
2392
- if (offset !== bytes.length) return null;
2393
- return chains;
2554
+ const positions = [];
2555
+ if (version === DECLARATION_V2) {
2556
+ if (offset >= bytes.length) return absent;
2557
+ const count = bytes[offset];
2558
+ offset += 1;
2559
+ for (let entry = 0; entry < count; entry++) {
2560
+ if (offset >= bytes.length) return absent;
2561
+ const length = bytes[offset];
2562
+ offset += 1;
2563
+ if (length === 0 || length > MAX_TOKEN_ID_BYTES || offset + length > bytes.length) return absent;
2564
+ let tokenId = 0n;
2565
+ for (const byte of bytes.subarray(offset, offset + length)) tokenId = tokenId << 8n | BigInt(byte);
2566
+ positions.push(tokenId);
2567
+ offset += length;
2568
+ }
2569
+ }
2570
+ if (offset !== bytes.length) return absent;
2571
+ return { acceptedSources: chains, uniswapV4Positions: positions };
2572
+ }
2573
+ function encodeAcceptedSourceChains(chains) {
2574
+ return encodePhantomBidDeclaration({ acceptedSourceChains: chains });
2575
+ }
2576
+ function decodeAcceptedSourceChains(paymasterAndData) {
2577
+ return decodePhantomBidDeclaration(paymasterAndData).acceptedSources;
2394
2578
  }
2395
2579
  function zipFillLegs(assets, outputs) {
2396
2580
  return assets.map((asset, index) => {
@@ -2475,17 +2659,46 @@ async function isDelegatedToSolverAccount(evmRpcUrl, account, solverAccount) {
2475
2659
  method: "eth_getCode",
2476
2660
  params: [account, "latest"]
2477
2661
  });
2478
- const code = typeof response.result === "string" ? response.result.toLowerCase() : "";
2662
+ if (typeof response.result !== "string") {
2663
+ throw new PhantomRpcError(`eth_getCode returned no code for ${account} on ${evmRpcUrl}`);
2664
+ }
2665
+ const code = response.result.toLowerCase();
2479
2666
  if (!code.startsWith(DELEGATION_INDICATOR_PREFIX)) return false;
2480
2667
  return `0x${code.slice(DELEGATION_INDICATOR_PREFIX.length)}` === solverAccount.toLowerCase();
2481
2668
  }
2669
+ function memoizedDelegationCheck() {
2670
+ const cache = /* @__PURE__ */ new Map();
2671
+ return (evmRpcUrl, account, solverAccount) => {
2672
+ const key = `${evmRpcUrl}|${account.toLowerCase()}|${solverAccount.toLowerCase()}`;
2673
+ let pending = cache.get(key);
2674
+ if (!pending) {
2675
+ pending = isDelegatedToSolverAccount(evmRpcUrl, account, solverAccount).catch((err) => {
2676
+ cache.delete(key);
2677
+ throw err;
2678
+ });
2679
+ cache.set(key, pending);
2680
+ }
2681
+ return pending;
2682
+ };
2683
+ }
2482
2684
  function evmChainId(chain) {
2483
2685
  const [prefix, id] = chain.split("-");
2484
2686
  if (prefix !== "EVM" || !id || !/^\d+$/.test(id)) return null;
2485
2687
  return BigInt(id);
2486
2688
  }
2487
2689
  async function isVerifiedSolverBid(params) {
2488
- const { userOp, commitment, sessionKey, chainId, solverAccount, evmRpcUrl, recoverSigner, bidNonceKey, logger } = params;
2690
+ const {
2691
+ userOp,
2692
+ commitment,
2693
+ sessionKey,
2694
+ chainId,
2695
+ solverAccount,
2696
+ evmRpcUrl,
2697
+ recoverSigner,
2698
+ bidNonceKey,
2699
+ isDelegated,
2700
+ logger
2701
+ } = params;
2489
2702
  const solver = userOp.sender;
2490
2703
  const parsed = splitBidSignature(userOp.signature);
2491
2704
  if (!parsed) {
@@ -2508,7 +2721,7 @@ async function isVerifiedSolverBid(params) {
2508
2721
  logger?.warn({ solver, commitment, signer }, "Rejecting phantom bid: signature does not recover to the sender");
2509
2722
  return false;
2510
2723
  }
2511
- if (!await isDelegatedToSolverAccount(evmRpcUrl, solver, solverAccount)) {
2724
+ if (!await isDelegated(evmRpcUrl, solver, solverAccount)) {
2512
2725
  logger?.warn({ solver, commitment, solverAccount }, "Rejecting phantom bid: sender is not a delegated solver");
2513
2726
  return false;
2514
2727
  }
@@ -2524,18 +2737,17 @@ async function fetchBidsForOrder(nodeUrl, commitment) {
2524
2737
  return Array.isArray(data.result) ? data.result : [];
2525
2738
  }
2526
2739
  async function ethCallUint(evmRpcUrl, to, data) {
2527
- try {
2528
- const result = await rpcCall(evmRpcUrl, {
2529
- id: 1,
2530
- jsonrpc: "2.0",
2531
- method: "eth_call",
2532
- params: [{ to, data }, "latest"]
2533
- });
2534
- if (result.error || !result.result || result.result === "0x") return 0n;
2535
- return BigInt(result.result);
2536
- } catch {
2537
- return 0n;
2740
+ const result = await rpcCall(evmRpcUrl, {
2741
+ id: 1,
2742
+ jsonrpc: "2.0",
2743
+ method: "eth_call",
2744
+ params: [{ to, data }, "latest"]
2745
+ });
2746
+ if (result.result === "0x") return 0n;
2747
+ if (typeof result.result !== "string") {
2748
+ throw new PhantomRpcError(`eth_call returned no result for ${to} on ${evmRpcUrl}`);
2538
2749
  }
2750
+ return BigInt(result.result);
2539
2751
  }
2540
2752
  async function getTotalSolverBalance(evmRpcUrl, chain, token, solver, yieldVaults) {
2541
2753
  const padded = solver.replace("0x", "").padStart(64, "0");
@@ -2547,27 +2759,104 @@ async function getTotalSolverBalance(evmRpcUrl, chain, token, solver, yieldVault
2547
2759
  );
2548
2760
  return vaultBalances.reduce((acc, b) => acc + b, raw);
2549
2761
  }
2762
+ var SELECTOR_OWNER_OF = "0x6352211e";
2763
+ var SELECTOR_POSITION_LIQUIDITY = "0x1efeed33";
2764
+ var SELECTOR_POOL_AND_POSITION_INFO = "0x7ba03aad";
2765
+ var SELECTOR_GET_SLOT0 = "0xc815641c";
2766
+ var uint256Arg = (value) => value.toString(16).padStart(64, "0");
2767
+ async function readV4Position(evmRpcUrl, contracts, tokenId, keccak, logger) {
2768
+ const call = async (to, data) => {
2769
+ const result = await rpcCall(evmRpcUrl, {
2770
+ id: 1,
2771
+ jsonrpc: "2.0",
2772
+ method: "eth_call",
2773
+ params: [{ to, data }, "latest"]
2774
+ });
2775
+ if (result.result === "0x") return null;
2776
+ if (typeof result.result !== "string") {
2777
+ throw new PhantomRpcError(`eth_call returned no result for ${to} on ${evmRpcUrl}`);
2778
+ }
2779
+ return result.result;
2780
+ };
2781
+ const arg = uint256Arg(tokenId);
2782
+ const [ownerData, infoData, liquidityData] = await Promise.all([
2783
+ call(contracts.positionManager, `${SELECTOR_OWNER_OF}${arg}`),
2784
+ call(contracts.positionManager, `${SELECTOR_POOL_AND_POSITION_INFO}${arg}`),
2785
+ call(contracts.positionManager, `${SELECTOR_POSITION_LIQUIDITY}${arg}`)
2786
+ ]);
2787
+ if (!ownerData) return null;
2788
+ if (!infoData || !liquidityData) {
2789
+ logger?.warn(
2790
+ { tokenId: tokenId.toString(), positionManager: contracts.positionManager },
2791
+ "Uniswap V4 position exists but its pool info or liquidity did not read back \u2014 check the configured PositionManager"
2792
+ );
2793
+ return null;
2794
+ }
2795
+ const info = decodePoolAndPositionInfo(infoData);
2796
+ const slot0Data = await call(contracts.stateView, `${SELECTOR_GET_SLOT0}${keccak(info.poolKeyEncoded).slice(2)}`);
2797
+ if (!slot0Data) {
2798
+ logger?.warn(
2799
+ { tokenId: tokenId.toString(), stateView: contracts.stateView, evmRpcUrl },
2800
+ "Uniswap V4 slot0 read returned nothing for a live position \u2014 the configured StateView address is wrong"
2801
+ );
2802
+ return null;
2803
+ }
2804
+ return {
2805
+ owner: `0x${ownerData.slice(-40)}`.toLowerCase(),
2806
+ info,
2807
+ liquidity: word(liquidityData, 0),
2808
+ sqrtPriceX96: word(slot0Data, 0)
2809
+ };
2810
+ }
2811
+ function memoizedV4Position(keccak, logger) {
2812
+ const cache = /* @__PURE__ */ new Map();
2813
+ return (evmRpcUrl, chain, contracts, tokenId) => {
2814
+ const key = `${chain}|${tokenId}`;
2815
+ let pending = cache.get(key);
2816
+ if (!pending) {
2817
+ pending = readV4Position(evmRpcUrl, contracts, tokenId, keccak, logger).catch((err) => {
2818
+ cache.delete(key);
2819
+ throw err;
2820
+ });
2821
+ cache.set(key, pending);
2822
+ }
2823
+ return pending;
2824
+ };
2825
+ }
2550
2826
  function memoizedSolverBalance(yieldVaults) {
2551
2827
  const cache = /* @__PURE__ */ new Map();
2552
2828
  return (evmRpcUrl, chain, token, solver) => {
2553
2829
  const key = `${chain}|${token.toLowerCase()}|${solver.toLowerCase()}`;
2554
2830
  let pending = cache.get(key);
2555
2831
  if (!pending) {
2556
- pending = getTotalSolverBalance(evmRpcUrl, chain, token, solver, yieldVaults);
2832
+ pending = getTotalSolverBalance(evmRpcUrl, chain, token, solver, yieldVaults).catch((err) => {
2833
+ cache.delete(key);
2834
+ throw err;
2835
+ });
2557
2836
  cache.set(key, pending);
2558
2837
  }
2559
2838
  return pending;
2560
2839
  };
2561
2840
  }
2562
- async function sweepSolverLiquidity(evmRpcUrls, yieldVaults, solver, getBalance) {
2841
+ async function sweepSolverLiquidity(evmRpcUrls, yieldVaults, solver, getBalance, positions) {
2563
2842
  const balances = [];
2564
2843
  for (const [chain, tokens] of Object.entries(yieldVaults)) {
2565
2844
  const url = evmRpcUrls[chain];
2566
2845
  if (!url) continue;
2567
2846
  for (const token of Object.keys(tokens)) {
2568
2847
  const balance = await getBalance(url, chain, token, solver);
2569
- if (balance === 0n) continue;
2570
- balances.push({ solver, chain, tokenAddress: token, balance });
2848
+ const uniswapV4Balance = chain === positions.chain ? positions.states.reduce(
2849
+ (total2, state) => total2 + positionAmountOfToken({
2850
+ info: state.info,
2851
+ liquidity: state.liquidity,
2852
+ sqrtPriceX96: state.sqrtPriceX96,
2853
+ outputToken: token
2854
+ }),
2855
+ 0n
2856
+ ) : 0n;
2857
+ const total = balance + uniswapV4Balance;
2858
+ if (total === 0n) continue;
2859
+ balances.push({ solver, chain, tokenAddress: token, balance: total });
2571
2860
  }
2572
2861
  }
2573
2862
  return balances;
@@ -2578,6 +2867,24 @@ function toAddress(token) {
2578
2867
  return `0x${addr}`;
2579
2868
  }
2580
2869
  async function aggregatePhantomBids(params) {
2870
+ const isDelegated = memoizedDelegationCheck();
2871
+ let lastErr;
2872
+ for (let attempt = 1; attempt <= AGGREGATION_ATTEMPTS; attempt++) {
2873
+ try {
2874
+ return await runAggregation(params, isDelegated);
2875
+ } catch (err) {
2876
+ lastErr = err;
2877
+ params.logger?.warn(
2878
+ { err, commitment: params.commitment, chain: params.chain, attempt, of: AGGREGATION_ATTEMPTS },
2879
+ "Phantom aggregation attempt failed"
2880
+ );
2881
+ if (attempt < AGGREGATION_ATTEMPTS) await new Promise((resolve) => setTimeout(resolve, 250 * attempt));
2882
+ }
2883
+ }
2884
+ throw lastErr;
2885
+ }
2886
+ var AGGREGATION_ATTEMPTS = 5;
2887
+ async function runAggregation(params, isDelegated) {
2581
2888
  const { nodeUrl, evmRpcUrls, chain, gatewayAddress, commitment, yieldVaults, solverAccount, logger } = params;
2582
2889
  const extractFill = params.extractFill ?? extractFillData;
2583
2890
  const recoverSigner = params.recoverSigner ?? recoverBidSignerViem;
@@ -2592,6 +2899,8 @@ async function aggregatePhantomBids(params) {
2592
2899
  }
2593
2900
  const bids = await fetchBidsForOrder(nodeUrl, commitment);
2594
2901
  if (bids.length === 0) return null;
2902
+ const v4Contracts = params.uniswapV4?.[chain];
2903
+ const readPosition = memoizedV4Position(params.keccak ?? viem.keccak256, logger);
2595
2904
  const getBalance = params.getBalance ?? memoizedSolverBalance(yieldVaults);
2596
2905
  const quotesByLeg = /* @__PURE__ */ new Map();
2597
2906
  const lpBalances = [];
@@ -2622,6 +2931,7 @@ async function aggregatePhantomBids(params) {
2622
2931
  evmRpcUrl: destUrl,
2623
2932
  recoverSigner,
2624
2933
  bidNonceKey,
2934
+ isDelegated,
2625
2935
  logger
2626
2936
  });
2627
2937
  if (!verified) continue;
@@ -2631,12 +2941,39 @@ async function aggregatePhantomBids(params) {
2631
2941
  continue;
2632
2942
  }
2633
2943
  countedSolvers.add(normalizedSolver);
2634
- const acceptedSources = decodeAcceptedSourceChains(decoded.paymasterAndData);
2944
+ const declaration = decodePhantomBidDeclaration(decoded.paymasterAndData);
2945
+ const acceptedSources = declaration.acceptedSources;
2635
2946
  const quotedLegs = [...fillData.legs.entries()].filter(([, leg]) => leg.solverAmount !== 0n);
2947
+ const declaredPositions = v4Contracts ? declaration.uniswapV4Positions : [];
2948
+ const positions = (await Promise.all(
2949
+ declaredPositions.map((tokenId) => readPosition(destUrl, chain, v4Contracts, tokenId))
2950
+ )).filter((state, index) => {
2951
+ if (!state) return false;
2952
+ if (state.owner !== normalizedSolver) {
2953
+ logger?.warn(
2954
+ { solver, commitment, tokenId: declaredPositions[index].toString(), owner: state.owner },
2955
+ "Ignoring declared Uniswap V4 position: not owned by the bidding solver"
2956
+ );
2957
+ return false;
2958
+ }
2959
+ return true;
2960
+ });
2636
2961
  const weights = await Promise.all(
2637
2962
  // Price influence: the solver's liquidity in THIS leg's output token on the destination
2638
2963
  // chain, so a leg is weighted by the inventory that actually backs it.
2639
- quotedLegs.map(([, leg]) => getBalance(destUrl, chain, toAddress(leg.outputToken), solver))
2964
+ quotedLegs.map(async ([, leg]) => {
2965
+ const outputToken = toAddress(leg.outputToken);
2966
+ const balance = await getBalance(destUrl, chain, outputToken, solver);
2967
+ return positions.reduce(
2968
+ (total, state) => total + positionAmountOfToken({
2969
+ info: state.info,
2970
+ liquidity: state.liquidity,
2971
+ sqrtPriceX96: state.sqrtPriceX96,
2972
+ outputToken
2973
+ }),
2974
+ balance
2975
+ );
2976
+ })
2640
2977
  );
2641
2978
  for (const [position, [legIndex, leg]] of quotedLegs.entries()) {
2642
2979
  const weight = weights[position];
@@ -2645,23 +2982,40 @@ async function aggregatePhantomBids(params) {
2645
2982
  entry.bidders.push({ solver: normalizedSolver, weight, acceptedSources });
2646
2983
  quotesByLeg.set(legIndex, entry);
2647
2984
  }
2648
- lpBalances.push(...await sweepSolverLiquidity(evmRpcUrls, yieldVaults, solver, getBalance));
2985
+ lpBalances.push(
2986
+ ...await sweepSolverLiquidity(evmRpcUrls, yieldVaults, solver, getBalance, {
2987
+ chain,
2988
+ states: positions
2989
+ })
2990
+ );
2649
2991
  } catch (err) {
2992
+ if (err instanceof PhantomRpcError) throw err;
2650
2993
  logger?.warn({ err, filler: bid.filler }, "Failed to process bid for price snapshot");
2651
2994
  }
2652
2995
  }
2653
2996
  if (quotesByLeg.size === 0) return null;
2654
- const legs = [...quotesByLeg.entries()].sort(([a], [b]) => a - b).map(([legIndex, { outputToken, quotes, bidders }]) => {
2655
- const medianPrice = weightedMedian(quotes);
2656
- return {
2657
- legIndex,
2658
- outputToken,
2659
- lowestPrice: medianPrice,
2660
- highestPrice: medianPrice,
2661
- medianPrice,
2662
- bidCount: quotes.length,
2663
- bidders
2664
- };
2997
+ const legs = [...quotesByLeg.entries()].sort(([a], [b]) => a - b).flatMap(([legIndex, { outputToken, quotes, bidders }]) => {
2998
+ const backedQuotes = quotes.filter((quote) => quote.weight > 0n);
2999
+ const backedBidders = bidders.filter((bidder) => bidder.weight > 0n);
3000
+ if (backedQuotes.length === 0) {
3001
+ logger?.warn(
3002
+ { commitment, chain, legIndex, outputToken, quotes: quotes.length },
3003
+ "Dropping phantom leg: no bidder holds the output token on this chain, so no quote is backed"
3004
+ );
3005
+ return [];
3006
+ }
3007
+ const medianPrice = weightedMedian(backedQuotes);
3008
+ return [
3009
+ {
3010
+ legIndex,
3011
+ outputToken,
3012
+ lowestPrice: medianPrice,
3013
+ highestPrice: medianPrice,
3014
+ medianPrice,
3015
+ bidCount: backedQuotes.length,
3016
+ bidders: backedBidders
3017
+ }
3018
+ ];
2665
3019
  });
2666
3020
  return { legs, lpBalances };
2667
3021
  }
@@ -2672,9 +3026,11 @@ exports.IntentGatewayV2 = IntentGatewayV2_default;
2672
3026
  exports.aggregatePhantomBids = aggregatePhantomBids;
2673
3027
  exports.decodeAcceptedSourceChains = decodeAcceptedSourceChains;
2674
3028
  exports.decodeERC7821ExecuteBatch = decodeERC7821ExecuteBatch;
3029
+ exports.decodePhantomBidDeclaration = decodePhantomBidDeclaration;
2675
3030
  exports.decodeUserOpScale = decodeUserOpScale;
2676
3031
  exports.encodeAcceptedSourceChains = encodeAcceptedSourceChains;
2677
3032
  exports.encodeERC7821ExecuteBatch = encodeERC7821ExecuteBatch;
3033
+ exports.encodePhantomBidDeclaration = encodePhantomBidDeclaration;
2678
3034
  exports.encodeUserOpScale = encodeUserOpScale;
2679
3035
  exports.extractFillData = extractFillData;
2680
3036
  exports.fetchBidsForOrder = fetchBidsForOrder;