@guru-fund/sdk 0.2.3 → 0.2.4

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.
@@ -1,5 +1,5 @@
1
1
  export declare const VELORA_ENDPOINT = "https://api.velora.xyz/swap";
2
- export declare const SUPPORTED_DEXS: readonly ["UniswapV2", "UniswapV3", "UniswapV4", "AerodromeV2", "PancakeSwapV2", "PancakeSwapV3"];
2
+ export declare const SUPPORTED_DEXS: readonly ["UniswapV2", "UniswapV3", "UniswapV4", "AerodromeV3", "AerodromeV2", "PancakeSwapV2", "PancakeSwapV3"];
3
3
  export declare const VELORA_VERSION_BY_DEX: Partial<Record<SupportedDex, string>>;
4
4
  export type SupportedDex = (typeof SUPPORTED_DEXS)[number];
5
5
  export declare const INITIAL_SLIPPAGE_E3 = 500n;
@@ -3,6 +3,7 @@ export const SUPPORTED_DEXS = [
3
3
  'UniswapV2',
4
4
  'UniswapV3',
5
5
  'UniswapV4',
6
+ 'AerodromeV3',
6
7
  'AerodromeV2',
7
8
  'PancakeSwapV2',
8
9
  'PancakeSwapV3',
@@ -1,4 +1,4 @@
1
- import { parseEther } from 'ethers';
1
+ import { Contract, parseEther, ZeroAddress } from 'ethers';
2
2
  import { getGuruProtocolAddresses, } from '../addresses.js';
3
3
  import compareAddresses from '../helpers/compareAddresses.js';
4
4
  import { Token } from '../helpers/Token.js';
@@ -7,6 +7,33 @@ import getUniswapV4Route, { connectV4Quoter, toAdapterPathKeys, } from './getUni
7
7
  import getVeloraRoute from './getVeloraRoute.js';
8
8
  import PoolHelper from './poolHelper.js';
9
9
  import { discoverV4Paths } from './v4PoolDiscovery.js';
10
+ const AERO_V3_FACTORY_ABI = [
11
+ 'function getPool(address tokenA, address tokenB, int24 tickSpacing) view returns (address pool)',
12
+ ];
13
+ const V2_FACTORY_ABI = [
14
+ 'function getPair(address tokenA, address tokenB) view returns (address pair)',
15
+ ];
16
+ const V2_ROUTER_ABI = [
17
+ 'function getAmountsOut(uint256 amountIn, address[] memory path) view returns (uint256[] memory amounts)',
18
+ ];
19
+ const AERO_V2_FACTORY_ABI = [
20
+ 'function getPool(address tokenA, address tokenB, bool stable) view returns (address)',
21
+ ];
22
+ const AERO_V2_ROUTER_ABI = [
23
+ 'function getAmountsOut(uint256 amountIn, (address from, address to, bool stable, address factory)[] routes) view returns (uint256[] amounts)',
24
+ ];
25
+ const V3_FACTORY_ABI = [
26
+ 'function getPool(address tokenA, address tokenB, uint24 fee) view returns (address pool)',
27
+ ];
28
+ const V3_QUOTER_ABI = [
29
+ 'function quoteExactInputSingle((address tokenIn,address tokenOut,uint256 amountIn,uint24 fee,uint160 sqrtPriceLimitX96) params) returns (uint256 amountOut,uint160 sqrtPriceX96After,uint32 initializedTicksCrossed,uint256 gasEstimate)',
30
+ ];
31
+ const AERO_V3_QUOTER_ABI = [
32
+ 'function quoteExactInputSingle((address tokenIn, address tokenOut, uint256 amountIn, int24 tickSpacing, uint160 sqrtPriceLimitX96) params) returns (uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate)',
33
+ ];
34
+ const UNISWAP_V3_FEES = [100, 500, 3000, 10000];
35
+ const PANCAKE_V3_FEES = [100, 500, 2500, 10000];
36
+ const AERODROME_V3_TICK_SPACINGS = [1, 50, 100, 200];
10
37
  export async function veloraThenV4Discovery(velora, v4Discovery) {
11
38
  try {
12
39
  return await velora();
@@ -97,9 +124,7 @@ export async function getPriceUsd1e18(token, ctx) {
97
124
  }
98
125
  const { decimals } = await new Token(token, ctx.provider).metadata();
99
126
  const oneToken = 10n ** BigInt(decimals);
100
- const stableUsd = await _priceViaDirectStablePool(token, oneToken, poolHelper, stable, stableDecimals);
101
- if (stableUsd !== null)
102
- return stableUsd;
127
+ const stableUsd = await _priceViaDirectStablePool(token, oneToken, poolHelper, stable, stableDecimals, ctx.provider);
103
128
  const WETH_UNIT = 10n ** 18n;
104
129
  try {
105
130
  const wethUsd = await getWethUsd();
@@ -108,33 +133,157 @@ export async function getPriceUsd1e18(token, ctx) {
108
133
  path: [token, addresses.tokens.WETH],
109
134
  requireSwappable: false,
110
135
  });
111
- return (oneTokenInWeth * wethUsd) / WETH_UNIT;
136
+ const wethRouteUsd = (oneTokenInWeth * wethUsd) / WETH_UNIT;
137
+ return stableUsd !== null && stableUsd > wethRouteUsd
138
+ ? stableUsd
139
+ : wethRouteUsd;
112
140
  }
113
141
  catch (error) {
114
142
  const v4Usd = await _priceViaV4Pools(token, oneToken, getWethUsd, ctx);
143
+ if (stableUsd !== null && v4Usd !== null) {
144
+ return stableUsd > v4Usd ? stableUsd : v4Usd;
145
+ }
146
+ if (stableUsd !== null)
147
+ return stableUsd;
115
148
  if (v4Usd !== null)
116
149
  return v4Usd;
117
150
  throw error;
118
151
  }
119
152
  }
120
- async function _priceViaDirectStablePool(token, oneToken, poolHelper, stable, stableDecimals) {
121
- const factory = poolHelper.addresses.factories.aerodromeV2;
122
- if (!factory)
123
- return null;
124
- const route = await poolHelper
125
- .getTokensForStableQuote({
126
- path: [token, stable],
127
- inputAmount: oneToken,
128
- slippage: 0n,
129
- exchangeFactory: factory,
130
- })
131
- .catch(() => null);
132
- if (!route)
133
- return null;
134
- const amountToReceive = BigInt(route.data.amountToReceive);
135
- if (amountToReceive === 0n)
153
+ async function _priceViaDirectStablePool(token, oneToken, poolHelper, stable, stableDecimals, provider) {
154
+ const stableUnit = Token.unitFor(stableDecimals);
155
+ const toUsd1e18 = (amount) => (amount * 10n ** 18n) / stableUnit;
156
+ const quotes = [];
157
+ const v2Amount = await _quoteDirectV2StablePools(token, stable, oneToken, poolHelper, provider);
158
+ if (v2Amount > 0n)
159
+ quotes.push(v2Amount);
160
+ const v3Amount = await _quoteDirectV3StablePools(token, stable, oneToken, poolHelper, provider);
161
+ if (v3Amount > 0n)
162
+ quotes.push(v3Amount);
163
+ const aeroV3Amount = await _quoteDirectAerodromeV3StablePool(token, stable, oneToken, poolHelper, provider);
164
+ if (aeroV3Amount > 0n)
165
+ quotes.push(aeroV3Amount);
166
+ if (quotes.length === 0)
136
167
  return null;
137
- return (amountToReceive * 10n ** 18n) / Token.unitFor(stableDecimals);
168
+ const best = quotes.reduce((currentBest, amount) => amount > currentBest ? amount : currentBest);
169
+ return toUsd1e18(best);
170
+ }
171
+ async function _quoteDirectV2StablePools(token, stable, oneToken, poolHelper, provider) {
172
+ let best = 0n;
173
+ const quoteStandardV2 = async (factoryAddress, routerAddress) => {
174
+ if (!factoryAddress || !routerAddress)
175
+ return;
176
+ const factory = new Contract(factoryAddress, V2_FACTORY_ABI, provider);
177
+ const pairAddress = await factory
178
+ .getPair(token, stable)
179
+ .catch(() => ZeroAddress);
180
+ if (pairAddress === ZeroAddress)
181
+ return;
182
+ const router = new Contract(routerAddress, V2_ROUTER_ABI, provider);
183
+ const amounts = await router
184
+ .getAmountsOut(oneToken, [token, stable])
185
+ .catch(() => null);
186
+ const amountOut = amounts?.at(-1) ?? 0n;
187
+ if (amountOut > best)
188
+ best = amountOut;
189
+ };
190
+ const quoteAerodromeV2 = async (factoryAddress, routerAddress) => {
191
+ if (!factoryAddress || !routerAddress)
192
+ return;
193
+ const factory = new Contract(factoryAddress, AERO_V2_FACTORY_ABI, provider);
194
+ const router = new Contract(routerAddress, AERO_V2_ROUTER_ABI, provider);
195
+ await Promise.all([false, true].map(async (stablePool) => {
196
+ const poolAddress = await factory
197
+ .getPool(token, stable, stablePool)
198
+ .catch(() => ZeroAddress);
199
+ if (poolAddress === ZeroAddress)
200
+ return;
201
+ const amounts = await router
202
+ .getAmountsOut(oneToken, [
203
+ {
204
+ from: token,
205
+ to: stable,
206
+ stable: stablePool,
207
+ factory: factoryAddress,
208
+ },
209
+ ])
210
+ .catch(() => null);
211
+ const amountOut = amounts?.at(-1) ?? 0n;
212
+ if (amountOut > best)
213
+ best = amountOut;
214
+ }));
215
+ };
216
+ await Promise.all([
217
+ quoteStandardV2(poolHelper.addresses.factories.uniswapV2, poolHelper.addresses.routers.uniswapV2),
218
+ quoteStandardV2(poolHelper.addresses.factories.pancakeV2, poolHelper.addresses.routers.pancakeV2),
219
+ quoteAerodromeV2(poolHelper.addresses.factories.aerodromeV2, poolHelper.addresses.routers.aerodromeV2),
220
+ ]);
221
+ return best;
222
+ }
223
+ async function _quoteDirectV3StablePools(token, stable, oneToken, poolHelper, provider) {
224
+ let best = 0n;
225
+ const quoteFactory = async (factoryAddress, quoterAddress, fees) => {
226
+ if (!factoryAddress || !quoterAddress)
227
+ return;
228
+ const factory = new Contract(factoryAddress, V3_FACTORY_ABI, provider);
229
+ const quoter = new Contract(quoterAddress, V3_QUOTER_ABI, provider);
230
+ await Promise.all(fees.map(async (fee) => {
231
+ const poolAddress = await factory
232
+ .getPool(token, stable, fee)
233
+ .catch(() => ZeroAddress);
234
+ if (poolAddress === ZeroAddress)
235
+ return;
236
+ const quote = await quoter.quoteExactInputSingle.staticCall({
237
+ tokenIn: token,
238
+ tokenOut: stable,
239
+ amountIn: oneToken,
240
+ fee: BigInt(fee),
241
+ sqrtPriceLimitX96: 0n,
242
+ }).catch(() => null);
243
+ const amountOut = quote?.amountOut ?? 0n;
244
+ if (amountOut > best)
245
+ best = amountOut;
246
+ }));
247
+ };
248
+ await Promise.all([
249
+ quoteFactory(poolHelper.addresses.factories.uniswapV3, poolHelper.addresses.quoters.uniswapV3, UNISWAP_V3_FEES),
250
+ quoteFactory(poolHelper.addresses.factories.pancakeV3, poolHelper.addresses.quoters.pancakeV3, PANCAKE_V3_FEES),
251
+ ]);
252
+ return best;
253
+ }
254
+ async function _quoteDirectAerodromeV3StablePool(token, stable, oneToken, poolHelper, provider) {
255
+ const quoterAddress = poolHelper.addresses.quoters.aerodromeV3;
256
+ if (!quoterAddress)
257
+ return 0n;
258
+ const factories = [
259
+ poolHelper.addresses.factories.aerodromeV3,
260
+ poolHelper.addresses.factories.aerodromeV3Bis,
261
+ ].filter((factory) => Boolean(factory));
262
+ if (factories.length === 0)
263
+ return 0n;
264
+ const quoter = new Contract(quoterAddress, AERO_V3_QUOTER_ABI, provider);
265
+ let best = 0n;
266
+ await Promise.all(factories.flatMap((factoryAddress) => {
267
+ const factory = new Contract(factoryAddress, AERO_V3_FACTORY_ABI, provider);
268
+ return AERODROME_V3_TICK_SPACINGS.map(async (tickSpacing) => {
269
+ const poolAddress = await factory
270
+ .getPool(token, stable, tickSpacing)
271
+ .catch(() => ZeroAddress);
272
+ if (poolAddress === ZeroAddress)
273
+ return;
274
+ const quote = await quoter.quoteExactInputSingle.staticCall({
275
+ tokenIn: token,
276
+ tokenOut: stable,
277
+ amountIn: oneToken,
278
+ tickSpacing: BigInt(tickSpacing),
279
+ sqrtPriceLimitX96: 0n,
280
+ }).catch(() => null);
281
+ const amountOut = quote?.amountOut ?? 0n;
282
+ if (amountOut > best)
283
+ best = amountOut;
284
+ });
285
+ }));
286
+ return best;
138
287
  }
139
288
  async function _priceViaV4Pools(token, oneToken, getWethUsd, ctx) {
140
289
  const addresses = getGuruProtocolAddresses(ctx.chainId);
@@ -32,10 +32,18 @@ export function extractPathFromResponse(dex, response) {
32
32
  const swapPath = swap.swapExchanges[0].data.path;
33
33
  if (!Array.isArray(swapPath) ||
34
34
  typeof swapPath[0]?.tokenIn !== 'string' ||
35
- typeof swapPath[0]?.fee !== 'string') {
35
+ swapPath[0]?.fee == null) {
36
36
  throw new Error('Unexpected V3 path structure');
37
37
  }
38
- combinedPath.push(...swapPath);
38
+ combinedPath.push(...swapPath.map((hop) => ({
39
+ tokenIn: hop.tokenIn,
40
+ tokenOut: hop.tokenOut,
41
+ fee: String(dex === 'AerodromeV3'
42
+ ? (hop.tickSpacing ?? hop.fee)
43
+ : hop.fee),
44
+ currentFee: hop.currentFee,
45
+ tickSpacing: hop.tickSpacing,
46
+ })));
39
47
  }
40
48
  return { type: 'v3', path: combinedPath, hops: combinedPath.length };
41
49
  }
@@ -21,6 +21,9 @@ const V2_PAIR_ABI = [
21
21
  const V3_FACTORY_ABI = [
22
22
  'function getPool(address tokenA, address tokenB, uint24 fee) view returns (address pool)',
23
23
  ];
24
+ const AERO_V3_FACTORY_ABI = [
25
+ 'function getPool(address tokenA, address tokenB, int24 tickSpacing) view returns (address pool)',
26
+ ];
24
27
  const V3_POOL_ABI = ['function fee() view returns (uint24)'];
25
28
  const AERO_FACTORY_ABI = [
26
29
  'function getPool(address tokenA, address tokenB, bool stable) view returns (address)',
@@ -260,7 +263,9 @@ export default class PoolHelper {
260
263
  }
261
264
  }
262
265
  else {
263
- const factory = connect(factoryAddress, V3_FACTORY_ABI, this.provider);
266
+ const factory = dex.kind === 'aerodrome'
267
+ ? connect(factoryAddress, AERO_V3_FACTORY_ABI, this.provider)
268
+ : connect(factoryAddress, V3_FACTORY_ABI, this.provider);
264
269
  await Promise.all(dex.feeTiers.map(async (feeTier) => {
265
270
  try {
266
271
  const poolAddress = await factory.getPool(token, weth, feeTier);
@@ -40,6 +40,8 @@ export type V3PathHop = {
40
40
  tokenIn: string;
41
41
  tokenOut: string;
42
42
  fee: string;
43
+ currentFee?: string | number;
44
+ tickSpacing?: string | number;
43
45
  };
44
46
  export type V3Path = V3PathHop[];
45
47
  export type CachedPath = {
@@ -14,6 +14,7 @@ interface V3DexEntry {
14
14
  interface VeloraDexConfig {
15
15
  UniswapV2?: V2DexEntry;
16
16
  AerodromeV2?: V2DexEntry;
17
+ AerodromeV3?: V3DexEntry;
17
18
  PancakeSwapV2?: V2DexEntry;
18
19
  UniswapV3?: V3DexEntry;
19
20
  PancakeSwapV3?: V3DexEntry;
@@ -1,6 +1,6 @@
1
1
  import { Contract, solidityPacked } from 'ethers';
2
2
  import { TOLL_DIVISOR_20BPS } from '../constants.js';
3
- import { AerodromeV2Adapter__factory, IPancakeQuoterV2__factory, UniswapV2Adapter__factory, UniswapV3Adapter__factory, UniswapV4Adapter__factory, } from '../typechain/index.js';
3
+ import { AerodromeV3Adapter__factory, AerodromeV2Adapter__factory, IPancakeQuoterV2__factory, UniswapV2Adapter__factory, UniswapV3Adapter__factory, UniswapV4Adapter__factory, } from '../typechain/index.js';
4
4
  import { connectV4Quoter, toAdapterPathKeys } from './getUniswapV4Route.js';
5
5
  import { SWAP_DEADLINE_SECONDS } from './constants.js';
6
6
  import { finalizeRouteQuote } from './finalizeRoute.js';
@@ -10,12 +10,18 @@ const V2_ROUTER_ABI = [
10
10
  const AERO_V2_ROUTER_ABI = [
11
11
  'function getAmountsOut(uint256 amountIn, (address from, address to, bool stable, address factory)[] routes) external view returns (uint256[] amounts)',
12
12
  ];
13
+ const AERO_V3_QUOTER_ABI = [
14
+ 'function quoteExactInputSingle((address tokenIn, address tokenOut, uint256 amountIn, int24 tickSpacing, uint160 sqrtPriceLimitX96) params) returns (uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate)',
15
+ ];
13
16
  function connectV2Router(address, provider) {
14
17
  return new Contract(address, V2_ROUTER_ABI, provider);
15
18
  }
16
19
  function connectAeroV2Router(address, provider) {
17
20
  return new Contract(address, AERO_V2_ROUTER_ABI, provider);
18
21
  }
22
+ function connectAeroV3Quoter(address, provider) {
23
+ return new Contract(address, AERO_V3_QUOTER_ABI, provider);
24
+ }
19
25
  function connectV3Quoter(address, provider) {
20
26
  return IPancakeQuoterV2__factory.connect(address, provider);
21
27
  }
@@ -33,6 +39,12 @@ export function buildVeloraDexConfig(addresses) {
33
39
  routerAddress: addresses.routers.aerodromeV2,
34
40
  };
35
41
  }
42
+ if (addresses.adapters.aerodromeV3 && addresses.quoters.aerodromeV3) {
43
+ cfg.AerodromeV3 = {
44
+ adapter: addresses.adapters.aerodromeV3,
45
+ quoterAddress: addresses.quoters.aerodromeV3,
46
+ };
47
+ }
36
48
  if (addresses.adapters.pancakeV2 && addresses.routers.pancakeV2) {
37
49
  cfg.PancakeSwapV2 = {
38
50
  adapter: addresses.adapters.pancakeV2,
@@ -212,6 +224,7 @@ export async function getRouteFromPath({ chainId, addresses, provider, simulator
212
224
  effectiveSlippageBps,
213
225
  };
214
226
  }
227
+ case 'AerodromeV3':
215
228
  case 'PancakeSwapV3':
216
229
  case 'UniswapV3': {
217
230
  if (cachedPath.type !== 'v3') {
@@ -222,12 +235,40 @@ export async function getRouteFromPath({ chainId, addresses, provider, simulator
222
235
  throw new Error(`DEX ${dex} not supported on chainId ${chainId}`);
223
236
  }
224
237
  const adapter = entry.adapter;
225
- const quoter = connectV3Quoter(entry.quoterAddress, provider);
226
- const encodedPath = encodeV3Path(cachedPath.path);
227
- const [{ amountOut: grossAmountToReceive }, blockNumber] = await Promise.all([
228
- quoter.quoteExactInput.staticCall(encodedPath, amountIn),
229
- provider.getBlockNumber(),
230
- ]);
238
+ let encodedPath;
239
+ let grossAmountToReceive;
240
+ let blockNumber;
241
+ if (dex === 'AerodromeV3') {
242
+ if (cachedPath.path.length !== 1) {
243
+ throw new Error('Aerodrome V3 multi-hop not supported');
244
+ }
245
+ const [hop] = cachedPath.path;
246
+ const tickSpacing = BigInt(hop.fee);
247
+ encodedPath = solidityPacked(['address', 'uint24', 'address'], [hop.tokenIn, Number(tickSpacing), hop.tokenOut]);
248
+ const quoter = connectAeroV3Quoter(entry.quoterAddress, provider);
249
+ const [quote, currentBlockNumber] = await Promise.all([
250
+ quoter.quoteExactInputSingle.staticCall({
251
+ tokenIn: hop.tokenIn,
252
+ tokenOut: hop.tokenOut,
253
+ amountIn,
254
+ tickSpacing,
255
+ sqrtPriceLimitX96: 0n,
256
+ }),
257
+ provider.getBlockNumber(),
258
+ ]);
259
+ grossAmountToReceive = quote.amountOut;
260
+ blockNumber = currentBlockNumber;
261
+ }
262
+ else {
263
+ const quoter = connectV3Quoter(entry.quoterAddress, provider);
264
+ encodedPath = encodeV3Path(cachedPath.path);
265
+ const [quote, currentBlockNumber] = await Promise.all([
266
+ quoter.quoteExactInput.staticCall(encodedPath, amountIn),
267
+ provider.getBlockNumber(),
268
+ ]);
269
+ grossAmountToReceive = quote.amountOut;
270
+ blockNumber = currentBlockNumber;
271
+ }
231
272
  let amountQuoted = grossAmountToReceive;
232
273
  let amountToSend = amountIn;
233
274
  if (toll.amount === 0n) {
@@ -238,7 +279,10 @@ export async function getRouteFromPath({ chainId, addresses, provider, simulator
238
279
  amountToSend += toll.amount;
239
280
  }
240
281
  const deadline = Math.floor(Date.now() / 1000) + SWAP_DEADLINE_SECONDS;
241
- const buildCallDataForAmount = (amountToReceive) => UniswapV3Adapter__factory.createInterface().encodeFunctionData('executeSwap', [
282
+ const swapInterface = dex === 'AerodromeV3'
283
+ ? AerodromeV3Adapter__factory.createInterface()
284
+ : UniswapV3Adapter__factory.createInterface();
285
+ const buildCallDataForAmount = (amountToReceive) => swapInterface.encodeFunctionData('executeSwap', [
242
286
  {
243
287
  amountToSend,
244
288
  amountToReceive,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guru-fund/sdk",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Guru Protocol SDK — quote + transaction builders for on-chain managed funds.",
5
5
  "license": "MIT",
6
6
  "type": "module",