@guru-fund/sdk 0.2.9 → 0.2.11

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.
@@ -22,6 +22,7 @@ export type GuruProtocolAddresses = {
22
22
  readonly factories: Readonly<Partial<Record<FactoryKey, string>>>;
23
23
  readonly routers: Readonly<Partial<Record<RouterKey, string>>>;
24
24
  readonly quoters: Readonly<Partial<Record<QuoterKey, string>>>;
25
+ readonly routeBridges?: readonly string[];
25
26
  readonly tokens: Readonly<{
26
27
  WETH: string;
27
28
  USDC: string;
package/dist/addresses.js CHANGED
@@ -121,6 +121,7 @@ const ROBINHOOD = {
121
121
  uniswapV3: '0x33e885ed0ec9bf04ecfb19341582aadcb4c8a9e7',
122
122
  uniswapV4: '0x8dc178efb8111bb0973dd9d722ebeff267c98f94',
123
123
  },
124
+ routeBridges: ['0xc6911796042b15d7fa4f6cde69e245ddcd3d9c31'],
124
125
  tokens: {
125
126
  WETH: '0x0bd7d308f8e1639fab988df18a8011f41eacad73',
126
127
  USDC: '0x5fc5360d0400a0fd4f2af552add042d716f1d168',
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export type { GuruProtocolAddresses, GuruProtocolChainId } from './addresses';
4
4
  export { GuruProtocol } from './GuruProtocol';
5
5
  export type { GuruProtocolOptions } from './GuruProtocol';
6
6
  export type { BuildDepositTxParams, BuildCloseTxParams, BuildHarvestTxParams, BuildTradeTxParams, BuildWithdrawTxParams, } from './txBuilders';
7
+ export type { BuildTradesTxParams } from './txBuilders/buildTradesTx';
7
8
  export type { QuoteDepositParams, QuoteDepositResult, QuoteDepositLogs, } from './quotes/quoteDeposit';
8
9
  export type { QuoteWithdrawalParams, QuoteWithdrawalResult, QuoteWithdrawalLogs, } from './quotes/quoteWithdrawal';
9
10
  export type { QuoteTradeParams, QuoteTradeResult } from './quotes/quoteTrade';
@@ -13,6 +13,8 @@ export interface QuoteTradeContext extends RouterContext {
13
13
  chainId: GuruProtocolChainId;
14
14
  }
15
15
  export interface QuoteTradeResult extends Route {
16
+ routes: Route[];
16
17
  txData: TransactionRequest;
17
18
  }
19
+ export declare function bestOfDirectAndComposite(direct: () => Promise<Route>, composite: () => Promise<Route[]>): Promise<Route[]>;
18
20
  export default function quoteTrade(params: QuoteTradeParams, ctx: QuoteTradeContext): Promise<QuoteTradeResult>;
@@ -1,9 +1,53 @@
1
1
  import { getGuruProtocolAddresses, } from '../addresses.js';
2
2
  import compareAddresses from '../helpers/compareAddresses.js';
3
3
  import { getRouteIn, getRouteOut } from '../router/index.js';
4
+ import { encodeVaultExecute } from '../router/simulation.js';
4
5
  import { quoteTradeSchema } from '../schemas/quoteTrade.js';
5
6
  import buildTradeTx from '../txBuilders/buildTradeTx.js';
7
+ import buildTradesTx from '../txBuilders/buildTradesTx.js';
6
8
  import { FundLedger__factory, Protocol__factory } from '../typechain/index.js';
9
+ async function buildCompositeWethRoute(routeParams, weth, controllerAddress, vaultAddress, ctx) {
10
+ const first = await getRouteIn({ ...routeParams, tokenOut: weth }, ctx);
11
+ const prefixTxs = [
12
+ {
13
+ from: controllerAddress,
14
+ to: vaultAddress,
15
+ callData: encodeVaultExecute(String(first.adapter), String(first.callData)),
16
+ },
17
+ ];
18
+ const second = await getRouteIn({
19
+ ...routeParams,
20
+ tokenIn: weth,
21
+ amountIn: BigInt(first.data.amountToReceive),
22
+ prefixTxs,
23
+ }, ctx);
24
+ return [first, second];
25
+ }
26
+ export async function bestOfDirectAndComposite(direct, composite) {
27
+ const [directResult, compositeResult] = await Promise.allSettled([
28
+ direct(),
29
+ composite(),
30
+ ]);
31
+ if (directResult.status === 'rejected' &&
32
+ directResult.reason instanceof Error &&
33
+ (directResult.reason.message.includes('TOKEN_LOCKED_BY_V4_HOOK') ||
34
+ directResult.reason.message.includes('UNISWAP_V4_HOOK_ROUTE_REVERTED'))) {
35
+ throw directResult.reason;
36
+ }
37
+ if (directResult.status === 'fulfilled' &&
38
+ compositeResult.status === 'fulfilled') {
39
+ const compositeRoute = compositeResult.value.at(-1);
40
+ return BigInt(directResult.value.data.amountToReceive) >=
41
+ BigInt(compositeRoute.data.amountToReceive)
42
+ ? [directResult.value]
43
+ : compositeResult.value;
44
+ }
45
+ if (directResult.status === 'fulfilled')
46
+ return [directResult.value];
47
+ if (compositeResult.status === 'fulfilled')
48
+ return compositeResult.value;
49
+ throw directResult.reason;
50
+ }
7
51
  export default async function quoteTrade(params, ctx) {
8
52
  const parsed = quoteTradeSchema.parse(params);
9
53
  const provider = ctx.provider;
@@ -27,25 +71,43 @@ export default async function quoteTrade(params, ctx) {
27
71
  ledger.controller(),
28
72
  ledger.manager(),
29
73
  ]);
30
- const route = await getRoute({
74
+ const slippageE2 = parsed.maxSlippage != null
75
+ ? Number(parsed.maxSlippage / 10n)
76
+ : undefined;
77
+ const routeParams = {
31
78
  chainId: ctx.chainId,
32
79
  tokenIn: parsed.tokenIn,
33
80
  tokenOut: parsed.tokenOut,
34
81
  amountIn: parsed.amountIn,
35
- slippageE2: parsed.maxSlippage != null
36
- ? Number(parsed.maxSlippage / 10n)
37
- : undefined,
82
+ slippageE2,
38
83
  account: vaultAddress,
39
84
  vault: vaultAddress,
40
- }, ctx);
41
- return {
42
- ...route,
43
- txData: buildTradeTx({
85
+ };
86
+ const weth = addresses.tokens.WETH;
87
+ const routes = compareAddresses(parsed.tokenIn, weth) ||
88
+ compareAddresses(parsed.tokenOut, weth)
89
+ ? [await getRoute(routeParams, ctx)]
90
+ : await bestOfDirectAndComposite(() => getRoute(routeParams, ctx), () => buildCompositeWethRoute(routeParams, weth, controllerAddress, vaultAddress, ctx));
91
+ const route = routes.at(-1);
92
+ const txData = routes.length === 1
93
+ ? buildTradeTx({
44
94
  controller: controllerAddress,
45
95
  ledger: parsed.ledger,
46
96
  adapter: String(route.adapter),
47
97
  callData: String(route.callData),
48
98
  from: manager,
49
- }),
99
+ })
100
+ : buildTradesTx({
101
+ controller: controllerAddress,
102
+ ledger: parsed.ledger,
103
+ adapters: routes.map((item) => String(item.adapter)),
104
+ callData: routes.map((item) => String(item.callData)),
105
+ from: manager,
106
+ });
107
+ return {
108
+ ...route,
109
+ routes,
110
+ hops: routes.reduce((sum, item) => sum + item.hops, 0),
111
+ txData,
50
112
  };
51
113
  }
@@ -2,6 +2,28 @@ import compareAddresses from '../helpers/compareAddresses.js';
2
2
  import { FundVault__factory } from '../typechain/index.js';
3
3
  import PoolHelper from './poolHelper.js';
4
4
  import { quoteWethTrade } from './quoteWethTrade.js';
5
+ async function tryWethBridgeV2Route(poolHelper, input, finalization) {
6
+ const factory = poolHelper.addresses.factories.uniswapV2;
7
+ if (!factory || !poolHelper.addresses.adapters.uniswapV2)
8
+ return null;
9
+ for (const bridge of poolHelper.addresses.routeBridges ?? []) {
10
+ try {
11
+ return await quoteWethTrade({
12
+ feeTier: 0,
13
+ input: {
14
+ ...input,
15
+ path: [input.tokenIn, bridge, input.tokenOut],
16
+ exchangeFactory: factory,
17
+ },
18
+ poolHelper,
19
+ finalization,
20
+ });
21
+ }
22
+ catch {
23
+ }
24
+ }
25
+ return null;
26
+ }
5
27
  async function tryDirectAerodromeV2RouteIn(poolHelper, { tokenIn, tokenOut, amountIn, slippage, finalization, }) {
6
28
  const factory = poolHelper.addresses.factories.aerodromeV2;
7
29
  if (!factory || !poolHelper.addresses.adapters.aerodromeV2)
@@ -61,14 +83,22 @@ export async function getFallbackRouteIn(params, ctx) {
61
83
  maxSlippageE3: slippage,
62
84
  };
63
85
  if (compareAddresses(tokenIn, weth) || compareAddresses(tokenOut, weth)) {
64
- const pairedToken = compareAddresses(tokenIn, weth) ? tokenOut : tokenIn;
65
- const coinTokenPool = await poolHelper.getUniswapCompatibleTokenPool(pairedToken);
66
- return quoteWethTrade({
67
- feeTier: coinTokenPool.feeTier,
68
- input: { tokenIn, tokenOut, amountIn, slippage },
69
- poolHelper,
70
- finalization,
71
- });
86
+ try {
87
+ const pairedToken = compareAddresses(tokenIn, weth) ? tokenOut : tokenIn;
88
+ const coinTokenPool = await poolHelper.getUniswapCompatibleTokenPool(pairedToken);
89
+ return await quoteWethTrade({
90
+ feeTier: coinTokenPool.feeTier,
91
+ input: { tokenIn, tokenOut, amountIn, slippage },
92
+ poolHelper,
93
+ finalization,
94
+ });
95
+ }
96
+ catch (directError) {
97
+ const bridged = await tryWethBridgeV2Route(poolHelper, { tokenIn, tokenOut, amountIn, slippage }, finalization);
98
+ if (bridged)
99
+ return bridged;
100
+ throw directError;
101
+ }
72
102
  }
73
103
  const directAerodrome = await tryDirectAerodromeV2RouteIn(poolHelper, {
74
104
  tokenIn,
@@ -113,15 +143,22 @@ export async function getFallbackRouteOut(params, ctx) {
113
143
  maxSlippageE3: slippage,
114
144
  };
115
145
  if (compareAddresses(tokenIn, weth) || compareAddresses(tokenOut, weth)) {
116
- const pairedToken = compareAddresses(tokenIn, weth) ? tokenOut : tokenIn;
117
- const coinTokenPool = await poolHelper.getUniswapCompatibleTokenPool(pairedToken);
118
- const route = await quoteWethTrade({
119
- feeTier: coinTokenPool.feeTier,
120
- input: { tokenIn, tokenOut, amountIn, slippage },
121
- poolHelper,
122
- finalization,
123
- });
124
- return { ...route, hops: 1 };
146
+ try {
147
+ const pairedToken = compareAddresses(tokenIn, weth) ? tokenOut : tokenIn;
148
+ const coinTokenPool = await poolHelper.getUniswapCompatibleTokenPool(pairedToken);
149
+ return await quoteWethTrade({
150
+ feeTier: coinTokenPool.feeTier,
151
+ input: { tokenIn, tokenOut, amountIn, slippage },
152
+ poolHelper,
153
+ finalization,
154
+ });
155
+ }
156
+ catch (directError) {
157
+ const bridged = await tryWethBridgeV2Route(poolHelper, { tokenIn, tokenOut, amountIn, slippage }, finalization);
158
+ if (bridged)
159
+ return bridged;
160
+ throw directError;
161
+ }
125
162
  }
126
163
  const directAerodrome = await tryDirectAerodromeV2RouteOut(poolHelper, {
127
164
  tokenIn,
@@ -7,6 +7,7 @@ import getUniswapV4Route, { connectV4Quoter, toAdapterPathKeys, } from './getUni
7
7
  import getVeloraRoute from './getVeloraRoute.js';
8
8
  import { stablecoinAddresses } from './helpers.js';
9
9
  import PoolHelper from './poolHelper.js';
10
+ import { V4_ZERO_ADDRESS } from './constants.js';
10
11
  import { discoverV4Paths } from './v4PoolDiscovery.js';
11
12
  const AERO_V3_FACTORY_ABI = [
12
13
  'function getPool(address tokenA, address tokenB, int24 tickSpacing) view returns (address pool)',
@@ -342,6 +343,11 @@ async function _priceViaV4Pools(token, oneToken, getWethUsd, ctx) {
342
343
  }
343
344
  return best > 0n ? best : null;
344
345
  };
346
+ const inNative = await quoteVia(V4_ZERO_ADDRESS);
347
+ if (inNative !== null) {
348
+ const wethUsd = await getWethUsd();
349
+ return (inNative * wethUsd) / WETH_UNIT;
350
+ }
345
351
  const inUsdc = await quoteVia(addresses.tokens.USDG ?? addresses.tokens.USDC);
346
352
  if (inUsdc !== null) {
347
353
  return inUsdc * 10n ** 12n;
@@ -52,13 +52,14 @@ export type QuoteRequest = {
52
52
  swapType?: SwapType;
53
53
  tokenAmount: bigint;
54
54
  slippage?: bigint;
55
- path: [string, string];
55
+ path: string[];
56
+ exchangeFactory?: string;
56
57
  requireSwappable?: boolean;
57
58
  };
58
59
  type VersionSpecificQuoteRequest = Omit<QuoteRequest, 'path' | 'slippage'> & {
59
60
  exchangeFactory: string;
60
61
  feeTier: FeeTier;
61
- path: [string, string];
62
+ path: string[];
62
63
  slippage: bigint;
63
64
  poolAddress?: string;
64
65
  };
@@ -115,7 +116,7 @@ export default class PoolHelper {
115
116
  exchangeFactory?: string;
116
117
  swappableOnly?: boolean;
117
118
  }): Promise<Pool>;
118
- getBestQuote({ swapType, tokenAmount, slippage, path, requireSwappable, }: QuoteRequest): Promise<Quote>;
119
+ getBestQuote({ swapType, tokenAmount, slippage, path, requireSwappable, exchangeFactory, }: QuoteRequest): Promise<Quote>;
119
120
  getV2Quote({ swapType, tokenAmount, exchangeFactory, path, feeTier, slippage, }: VersionSpecificQuoteRequest): Promise<Quote>;
120
121
  getV3Quote({ swapType, tokenAmount, exchangeFactory, path, feeTier, slippage, poolAddress, }: VersionSpecificQuoteRequest): Promise<Quote>;
121
122
  getStableForTokenQuote({ path, inputAmount, slippage, exchangeFactory, finalization, }: StableQuoteRequest): Promise<StableQuoteResult>;
@@ -327,7 +327,7 @@ export default class PoolHelper {
327
327
  }
328
328
  return pools[0];
329
329
  }
330
- async getBestQuote({ swapType = SwapType.EXACT_INPUT, tokenAmount, slippage = 500n, path, requireSwappable = true, }) {
330
+ async getBestQuote({ swapType = SwapType.EXACT_INPUT, tokenAmount, slippage = 500n, path, requireSwappable = true, exchangeFactory, }) {
331
331
  const weth = this.addresses.tokens.WETH;
332
332
  if (!path.some((t) => compareAddresses(t, weth))) {
333
333
  throw new Error(`${PoolHelperError.NO_ROUTE_FOUND}: WETH not in path`);
@@ -335,6 +335,7 @@ export default class PoolHelper {
335
335
  const token = path.find((t) => !compareAddresses(t, weth));
336
336
  const pools = await this.getUniswapCompatibleTokenPools(token, {
337
337
  swappableOnly: requireSwappable,
338
+ exchangeFactory,
338
339
  });
339
340
  if (pools.length === 0) {
340
341
  throw new Error(`${PoolHelperError.NO_ROUTE_FOUND}: token=${token}`);
@@ -406,6 +407,9 @@ export default class PoolHelper {
406
407
  };
407
408
  }
408
409
  async getV3Quote({ swapType = SwapType.EXACT_INPUT, tokenAmount, exchangeFactory, path, feeTier, slippage, poolAddress, }) {
410
+ if (path.length !== 2) {
411
+ throw new Error(`${PoolHelperError.EXCHANGE_INCOMPATIBLE}: V3 multi-hop requires per-hop fee tiers`);
412
+ }
409
413
  const [$INPUT, $OUTPUT] = path;
410
414
  const dex = this.getDexData(exchangeFactory);
411
415
  if (dex.type !== 'v3') {
@@ -811,7 +815,8 @@ export default class PoolHelper {
811
815
  }
812
816
  _adjustTokenAmountForSwapFee(tokenAmount, swapFeePercentage, swapType, path) {
813
817
  const weth = this.addresses.tokens.WETH;
814
- const shouldAdjust = compareAddresses(path[Number(swapType)], weth);
818
+ const tokenEndpoint = swapType === SwapType.EXACT_INPUT ? path[0] : path.at(-1);
819
+ const shouldAdjust = compareAddresses(tokenEndpoint, weth);
815
820
  if (!shouldAdjust) {
816
821
  return { adjustedTokenAmount: tokenAmount, swapFee: 0n };
817
822
  }
@@ -826,7 +831,8 @@ export default class PoolHelper {
826
831
  const sign = swapType === SwapType.EXACT_INPUT ? -1n : 1n;
827
832
  const slippageTolerance = calcSlippageTolerance(quote, slippage) * sign;
828
833
  const quoteWithSlippage = quote + slippageTolerance;
829
- const shouldAdjustQuote = !compareAddresses(path[Number(swapType)], weth);
834
+ const quoteEndpoint = swapType === SwapType.EXACT_INPUT ? path.at(-1) : path[0];
835
+ const shouldAdjustQuote = compareAddresses(quoteEndpoint, weth);
830
836
  if (!shouldAdjustQuote) {
831
837
  return { adjustedQuote: quoteWithSlippage, swapFee: 0n };
832
838
  }
@@ -7,6 +7,8 @@ export interface QuoteWethTradeParams {
7
7
  tokenOut: string;
8
8
  amountIn: bigint;
9
9
  slippage: bigint;
10
+ path?: string[];
11
+ exchangeFactory?: string;
10
12
  };
11
13
  poolHelper: PoolHelper;
12
14
  finalization?: PoolRouteFinalizationContext;
@@ -4,6 +4,7 @@ import { AerodromeV2Adapter__factory, UniswapV2Adapter__factory, UniswapV3Adapte
4
4
  import { finalizeRouteQuote } from './finalizeRoute.js';
5
5
  export async function quoteWethTrade({ feeTier, input, poolHelper, finalization, }) {
6
6
  const weth = poolHelper.addresses.tokens.WETH;
7
+ const routePath = input.path ?? [input.tokenIn, input.tokenOut];
7
8
  const wethIn = compareAddresses(input.tokenIn, weth);
8
9
  const wethOut = compareAddresses(input.tokenOut, weth);
9
10
  if (!wethIn && !wethOut) {
@@ -11,8 +12,9 @@ export async function quoteWethTrade({ feeTier, input, poolHelper, finalization,
11
12
  }
12
13
  const quote = await poolHelper.getBestQuote({
13
14
  tokenAmount: input.amountIn,
14
- path: [input.tokenIn, input.tokenOut],
15
+ path: routePath,
15
16
  slippage: finalization ? 0n : input.slippage,
17
+ exchangeFactory: input.exchangeFactory,
16
18
  });
17
19
  const adapter = poolHelper.getLotusAdapter(quote.exchangeFactory);
18
20
  const dex = poolHelper.getDexData(quote.exchangeFactory);
@@ -46,7 +48,7 @@ export async function quoteWethTrade({ feeTier, input, poolHelper, finalization,
46
48
  const data = {
47
49
  amountToSend: input.amountIn,
48
50
  amountToReceive,
49
- path: [input.tokenIn, input.tokenOut],
51
+ path: routePath,
50
52
  deadline,
51
53
  };
52
54
  return {
@@ -86,7 +88,7 @@ export async function quoteWethTrade({ feeTier, input, poolHelper, finalization,
86
88
  adapter,
87
89
  account: finalization.account,
88
90
  path: dex.type === 'v2'
89
- ? [input.tokenIn, input.tokenOut]
91
+ ? routePath
90
92
  : [
91
93
  {
92
94
  tokenIn: input.tokenIn,
@@ -119,7 +121,7 @@ export async function quoteWethTrade({ feeTier, input, poolHelper, finalization,
119
121
  currency: weth,
120
122
  amount: finalized.finalTollAmount,
121
123
  },
122
- hops: 1,
124
+ hops: routePath.length - 1,
123
125
  effectiveSlippageBps: finalized.effectiveSlippageBps,
124
126
  };
125
127
  }
@@ -40,10 +40,14 @@ export async function findMaxPassingAmountToReceive({ chainId, blockNumber, cont
40
40
  return success ? high : amountWithSlippage;
41
41
  }
42
42
  const candidates = generateCandidates(high, low, BUNDLE_SIMULATION_CANDIDATES);
43
- const results = await Promise.all(candidates.map((amount) => simulate(encodeVaultExecute(adapter, buildCallDataForAmount(amount)))));
44
- const successIndices = results
45
- .map((result, index) => (result.success ? index : -1))
46
- .filter((index) => index !== -1);
43
+ const successIndices = [];
44
+ for (const [index, amount] of candidates.entries()) {
45
+ const result = await simulate(encodeVaultExecute(adapter, buildCallDataForAmount(amount)));
46
+ if (result.success)
47
+ successIndices.push(index);
48
+ if (successIndices.length === 2)
49
+ break;
50
+ }
47
51
  if (successIndices.length === 0) {
48
52
  console.warn('Found no successful simulations for executable route quote', {
49
53
  chainId,
@@ -23,9 +23,11 @@ interface DexscreenerPair {
23
23
  liquidity?: {
24
24
  usd?: number;
25
25
  };
26
+ pairCreatedAt?: number;
26
27
  }
27
28
  export declare function selectV4PairPoolIds(pairs: DexscreenerPair[], tokenIn: string, tokenOut: string): string[];
28
29
  export declare function stitchNativeBridgeV4Paths(firstLegs: V4Path[], secondLegs: V4Path[]): V4Path[];
29
30
  export declare function discoverV4Paths(chainId: GuruProtocolChainId, tokenIn: string, tokenOut: string, provider: Provider, endpoint?: string): Promise<V4Path[]>;
31
+ export declare function findKnownV4BridgeKeys(chainId: GuruProtocolChainId, tokenIn: string, tokenOut: string): V4PoolKey[];
30
32
  export declare function _clearV4DiscoveryCache(): void;
31
33
  export {};
@@ -19,8 +19,37 @@ const V4_CHAIN_CONFIG = {
19
19
  deployBlock: 0,
20
20
  },
21
21
  };
22
+ const KNOWN_V4_BRIDGE_KEYS = {
23
+ 4663: [
24
+ {
25
+ currency0: V4_ZERO_ADDRESS,
26
+ currency1: '0x5fc5360d0400a0fd4f2af552add042d716f1d168',
27
+ fee: 25,
28
+ tickSpacing: 1,
29
+ hooks: V4_ZERO_ADDRESS,
30
+ },
31
+ {
32
+ currency0: V4_ZERO_ADDRESS,
33
+ currency1: '0x5fc5360d0400a0fd4f2af552add042d716f1d168',
34
+ fee: 100,
35
+ tickSpacing: 1,
36
+ hooks: V4_ZERO_ADDRESS,
37
+ },
38
+ {
39
+ currency0: V4_ZERO_ADDRESS,
40
+ currency1: '0x5fc5360d0400a0fd4f2af552add042d716f1d168',
41
+ fee: 500,
42
+ tickSpacing: 10,
43
+ hooks: V4_ZERO_ADDRESS,
44
+ },
45
+ ],
46
+ };
22
47
  const POOL_INITIALIZE_TOPIC = '0xdd466e674ea557f56295e2d0218a125ea4b4f0f6f3307b95f85e6110838d6438';
23
48
  export function isSafeDiscoveredV4PoolKey(key) {
49
+ const dynamicFeeFlag = 0x800000;
50
+ if (key.fee === dynamicFeeFlag) {
51
+ return !compareAddresses(key.hooks, V4_ZERO_ADDRESS);
52
+ }
24
53
  return key.fee >= 0 && key.fee <= V4_DISCOVERY_MAX_LP_FEE;
25
54
  }
26
55
  export function computeV4PoolId(key) {
@@ -66,18 +95,63 @@ export function stitchNativeBridgeV4Paths(firstLegs, secondLegs) {
66
95
  return paths;
67
96
  }
68
97
  const poolKeyCache = new Map();
69
- async function resolvePoolKey(chainId, poolId, provider) {
98
+ async function resolvePoolKey(chainId, poolId, provider, pairCreatedAt) {
70
99
  const cacheKey = `${chainId}:${poolId}`;
71
100
  const cached = poolKeyCache.get(cacheKey);
72
101
  if (cached)
73
102
  return cached;
74
103
  const config = V4_CHAIN_CONFIG[chainId];
75
- const logs = await provider.getLogs({
104
+ const filter = {
76
105
  address: config.poolManager,
77
106
  topics: [POOL_INITIALIZE_TOPIC, poolId],
78
107
  fromBlock: config.deployBlock,
79
108
  toBlock: 'latest',
80
- });
109
+ };
110
+ let logs;
111
+ try {
112
+ logs = await provider.getLogs(filter);
113
+ }
114
+ catch {
115
+ logs = [];
116
+ const latestBlock = await provider.getBlockNumber();
117
+ const chunkSize = 9_999;
118
+ if (pairCreatedAt) {
119
+ let low = config.deployBlock;
120
+ let high = latestBlock;
121
+ const targetTimestamp = Math.floor(pairCreatedAt / 1_000);
122
+ while (low < high) {
123
+ const middle = Math.floor((low + high) / 2);
124
+ const block = await provider.getBlock(middle);
125
+ if (!block)
126
+ break;
127
+ if (block.timestamp < targetTimestamp)
128
+ low = middle + 1;
129
+ else
130
+ high = middle;
131
+ }
132
+ const fromBlock = Math.max(config.deployBlock, low - 5_000);
133
+ const toBlock = Math.min(latestBlock, fromBlock + chunkSize);
134
+ logs = await provider.getLogs({
135
+ ...filter,
136
+ fromBlock,
137
+ toBlock,
138
+ });
139
+ }
140
+ if (logs.length === 0) {
141
+ for (let toBlock = latestBlock; toBlock >= config.deployBlock; toBlock -= chunkSize + 1) {
142
+ const fromBlock = Math.max(config.deployBlock, toBlock - chunkSize);
143
+ const chunk = await provider.getLogs({
144
+ ...filter,
145
+ fromBlock,
146
+ toBlock,
147
+ });
148
+ if (chunk.length > 0) {
149
+ logs = chunk;
150
+ break;
151
+ }
152
+ }
153
+ }
154
+ }
81
155
  const log = logs[0];
82
156
  if (!log)
83
157
  return null;
@@ -142,6 +216,7 @@ async function discoverDirectV4Paths(chainId, tokenIn, tokenOut, provider, endpo
142
216
  const isMajor = (token) => majors.some((major) => compareAddresses(major, token));
143
217
  const queryOrder = isMajor(tokenIn) ? [tokenOut, tokenIn] : [tokenIn, tokenOut];
144
218
  let poolIds = [];
219
+ const pairCreatedAtByPoolId = new Map();
145
220
  for (const queryToken of queryOrder) {
146
221
  const response = await fetch(`${endpoint}/${config.dexscreenerSlug}/${queryToken}`);
147
222
  if (!response.ok) {
@@ -149,15 +224,23 @@ async function discoverDirectV4Paths(chainId, tokenIn, tokenOut, provider, endpo
149
224
  }
150
225
  const pairs = (await response.json());
151
226
  poolIds = selectV4PairPoolIds(pairs, tokenIn, tokenOut);
152
- if (poolIds.length > 0)
227
+ if (poolIds.length > 0) {
228
+ for (const pair of pairs) {
229
+ if (!pair.pairAddress || !pair.pairCreatedAt)
230
+ continue;
231
+ pairCreatedAtByPoolId.set(pair.pairAddress.toLowerCase(), pair.pairCreatedAt);
232
+ }
153
233
  break;
234
+ }
154
235
  }
155
- const keys = await Promise.all(poolIds.map((poolId) => resolvePoolKey(chainId, poolId, provider)));
236
+ const keys = await Promise.all(poolIds.map((poolId) => resolvePoolKey(chainId, poolId, provider, pairCreatedAtByPoolId.get(poolId))));
156
237
  const resolvedKeys = keys.filter((key) => key !== null && isSafeDiscoveredV4PoolKey(key));
157
- const onchainKeys = resolvedKeys.length > 0
238
+ const knownKeys = findKnownV4BridgeKeys(chainId, tokenIn, tokenOut);
239
+ const onchainKeys = resolvedKeys.length > 0 || knownKeys.length > 0
158
240
  ? []
159
- : await discoverOnchainDirectV4PoolKeys(chainId, tokenIn, tokenOut, provider);
241
+ : await discoverOnchainDirectV4PoolKeys(chainId, tokenIn, tokenOut, provider).catch(() => []);
160
242
  const paths = resolvedKeys
243
+ .concat(knownKeys)
161
244
  .concat(onchainKeys)
162
245
  .filter((key) => key !== null)
163
246
  .map((key) => [
@@ -172,6 +255,12 @@ async function discoverDirectV4Paths(chainId, tokenIn, tokenOut, provider, endpo
172
255
  ]);
173
256
  return paths;
174
257
  }
258
+ export function findKnownV4BridgeKeys(chainId, tokenIn, tokenOut) {
259
+ return (KNOWN_V4_BRIDGE_KEYS[chainId] ?? []).filter((key) => (compareAddresses(key.currency0, tokenIn) &&
260
+ compareAddresses(key.currency1, tokenOut)) ||
261
+ (compareAddresses(key.currency0, tokenOut) &&
262
+ compareAddresses(key.currency1, tokenIn)));
263
+ }
175
264
  async function discoverOnchainDirectV4PoolKeys(chainId, tokenIn, tokenOut, provider) {
176
265
  const config = V4_CHAIN_CONFIG[chainId];
177
266
  const [currency0, currency1] = tokenIn.toLowerCase() < tokenOut.toLowerCase()
@@ -0,0 +1,9 @@
1
+ import type { TransactionRequest } from 'ethers';
2
+ export interface BuildTradesTxParams {
3
+ controller: string;
4
+ ledger: string;
5
+ adapters: string[];
6
+ callData: string[];
7
+ from: string;
8
+ }
9
+ export default function buildTradesTx(params: BuildTradesTxParams): TransactionRequest;
@@ -0,0 +1,9 @@
1
+ import { FundController__factory } from '../typechain/index.js';
2
+ export default function buildTradesTx(params) {
3
+ const data = FundController__factory.createInterface().encodeFunctionData('executeTrades', [params.ledger, params.adapters, params.callData]);
4
+ return {
5
+ to: params.controller,
6
+ from: params.from,
7
+ data,
8
+ };
9
+ }
@@ -7,4 +7,6 @@ export type { BuildWithdrawTxParams } from './buildWithdrawTx';
7
7
  export { default as buildHarvestTx } from './buildHarvestTx';
8
8
  export type { BuildHarvestTxParams } from './buildHarvestTx';
9
9
  export { default as buildTradeTx } from './buildTradeTx';
10
+ export { default as buildTradesTx } from './buildTradesTx';
10
11
  export type { BuildTradeTxParams } from './buildTradeTx';
12
+ export type { BuildTradesTxParams } from './buildTradesTx';
@@ -3,3 +3,4 @@ export { default as buildCloseTx } from './buildCloseTx.js';
3
3
  export { default as buildWithdrawTx } from './buildWithdrawTx.js';
4
4
  export { default as buildHarvestTx } from './buildHarvestTx.js';
5
5
  export { default as buildTradeTx } from './buildTradeTx.js';
6
+ export { default as buildTradesTx } from './buildTradesTx.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guru-fund/sdk",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "Guru Protocol SDK — quote + transaction builders for on-chain managed funds.",
5
5
  "license": "MIT",
6
6
  "type": "module",