Skip to content

Commit 6690b97

Browse files
committed
add connect2 strategy
1 parent 60fbdb8 commit 6690b97

File tree

5 files changed

+282
-4
lines changed

5 files changed

+282
-4
lines changed

src/swapService/interface.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ export interface StrategyMatchConfig {
7474
tokensInOrOut?: Address[]
7575
excludeTokensInOrOut?: Address[]
7676
repayVaults?: Address[]
77+
trades?: {
78+
tokenIn: Address
79+
tokenOut: Address
80+
}[]
7781
}
7882

7983
export interface RoutingItem {

src/swapService/strategies/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { StrategyBalmySDK } from "./strategyBalmySDK"
22
import { StrategyCombinedUniswap } from "./strategyCombinedUniswap"
3+
import { StrategyConnect2 } from "./strategyConnect2"
34
import { StrategyCurveLPNG } from "./strategyCurveLPNG"
45
import { StrategyERC4626Wrapper } from "./strategyERC4626Wrapper"
56
import { StrategyIdleCDOTranche } from "./strategyIdleCDOTranche"
@@ -16,6 +17,7 @@ export {
1617
StrategyIdleCDOTranche,
1718
StrategyCurveLPNG,
1819
StrategyRedirectDepositWrapper,
20+
StrategyConnect2,
1921
}
2022

2123
export const strategies = {
@@ -27,4 +29,5 @@ export const strategies = {
2729
[StrategyIdleCDOTranche.name()]: StrategyIdleCDOTranche,
2830
[StrategyCurveLPNG.name()]: StrategyCurveLPNG,
2931
[StrategyRedirectDepositWrapper.name()]: StrategyRedirectDepositWrapper,
32+
[StrategyConnect2.name()]: StrategyConnect2,
3033
}

src/swapService/strategies/strategyBalmySDK.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,13 @@ import { BINARY_SEARCH_TIMEOUT_SECONDS } from "../config/constants"
2727
import { type SwapApiResponseMulticallItem, SwapperMode } from "../interface"
2828
import type { StrategyResult, SwapParams, SwapQuote } from "../types"
2929
import {
30-
SWAPPER_HANDLER_GENERIC,
3130
adjustForInterest,
3231
binarySearchQuote,
3332
buildApiResponseExactInputFromQuote,
3433
buildApiResponseSwap,
3534
buildApiResponseVerifyDebtMax,
3635
calculateEstimatedAmountFrom,
3736
encodeApproveMulticallItem,
38-
encodeDepositMulticallItem,
39-
encodeRepayMulticallItem,
40-
encodeSwapMulticallItem,
4137
encodeTargetDebtAsExactInMulticall,
4238
isExactInRepay,
4339
matchParams,
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
import { type Address, parseUnits } from "viem"
2+
import { BINARY_SEARCH_TIMEOUT_SECONDS } from "../config/constants"
3+
import {
4+
type SwapApiResponse,
5+
type SwapApiResponseMulticallItem,
6+
SwapperMode,
7+
} from "../interface"
8+
import { runPipeline } from "../runner"
9+
import type { StrategyResult, SwapParams, SwapQuote } from "../types"
10+
import {
11+
adjustForInterest,
12+
binarySearchQuote,
13+
buildApiResponseSwap,
14+
buildApiResponseVerifyDebtMax,
15+
calculateEstimatedAmountFrom,
16+
encodeDepositMulticallItem,
17+
encodeRepayAndSweep,
18+
findToken,
19+
isExactInRepay,
20+
matchParams,
21+
promiseWithTimeout,
22+
} from "../utils"
23+
24+
type Connect2Config = {
25+
chainId: number
26+
connector: Address
27+
connectorDustVault: Address
28+
}
29+
30+
// Wrapper which stitches 2 swaps together through a configured connector asset
31+
export class StrategyConnect2 {
32+
static name() {
33+
return "connect2"
34+
}
35+
readonly match
36+
readonly config: Connect2Config
37+
38+
constructor(match = {}, config?: Connect2Config) {
39+
if (!config) throw new Error("StrategyConnect2 missing config")
40+
this.match = match
41+
this.config = config
42+
}
43+
44+
async supports(swapParams: SwapParams) {
45+
return (
46+
!isExactInRepay(swapParams) && swapParams.chainId === this.config.chainId
47+
)
48+
}
49+
50+
async findSwap(swapParams: SwapParams): Promise<StrategyResult> {
51+
const result: StrategyResult = {
52+
strategy: StrategyConnect2.name(),
53+
supports: await this.supports(swapParams),
54+
match: matchParams(swapParams, this.match),
55+
}
56+
57+
if (!result.supports || !result.match) return result
58+
59+
try {
60+
switch (swapParams.swapperMode) {
61+
case SwapperMode.EXACT_IN: {
62+
result.quotes = await this.exactIn(swapParams)
63+
break
64+
}
65+
case SwapperMode.TARGET_DEBT: {
66+
result.quotes = await this.targetDebt(swapParams)
67+
break
68+
}
69+
default: {
70+
result.error = "Unsupported swap mode"
71+
}
72+
}
73+
} catch (error) {
74+
result.error = error
75+
}
76+
77+
return result
78+
}
79+
80+
async exactIn(swapParams: SwapParams): Promise<SwapApiResponse[]> {
81+
const connectorToken = findToken(swapParams.chainId, this.config.connector)
82+
if (!connectorToken) throw new Error("Connector token not found")
83+
84+
const toConnectorSwapParams = {
85+
...swapParams,
86+
tokenOut: connectorToken,
87+
}
88+
89+
const toConnectorSwaps = await runPipeline(toConnectorSwapParams)
90+
91+
if (toConnectorSwaps.length === 0)
92+
throw new Error("To connector quotes not found")
93+
94+
//TODO fix for multiple results, taking the first one for now
95+
const toConnectorSwap = toConnectorSwaps[0]
96+
97+
const fromConnectorSwapParams = {
98+
...swapParams,
99+
tokenIn: connectorToken,
100+
amount: BigInt(toConnectorSwap.amountOutMin),
101+
}
102+
103+
const fromConnectorSwaps = await runPipeline(fromConnectorSwapParams)
104+
if (fromConnectorSwaps.length === 0)
105+
throw new Error("From connector quotes not found")
106+
107+
return fromConnectorSwaps.map((fromConnectorSwap) => {
108+
const connectorDustDepositMulticallItem = encodeDepositMulticallItem(
109+
this.config.connector,
110+
this.config.connectorDustVault,
111+
5n, // avoid zero shares
112+
swapParams.dustAccount,
113+
)
114+
115+
const multicallItems = [
116+
...toConnectorSwap.swap.multicallItems,
117+
...fromConnectorSwap.swap.multicallItems,
118+
connectorDustDepositMulticallItem,
119+
]
120+
121+
const swap = buildApiResponseSwap(swapParams.from, multicallItems)
122+
const verify = fromConnectorSwap.verify
123+
124+
return {
125+
amountIn: String(swapParams.amount),
126+
amountInMax: String(swapParams.amount),
127+
amountOut: fromConnectorSwap.amountOut,
128+
amountOutMin: fromConnectorSwap.amountOutMin,
129+
vaultIn: swapParams.vaultIn,
130+
receiver: swapParams.receiver,
131+
accountIn: swapParams.accountIn,
132+
accountOut: swapParams.accountOut,
133+
tokenIn: swapParams.tokenIn,
134+
tokenOut: swapParams.tokenOut,
135+
slippage: swapParams.slippage,
136+
route: [...toConnectorSwap.route, ...fromConnectorSwap.route],
137+
swap,
138+
verify,
139+
}
140+
})
141+
}
142+
143+
async targetDebt(swapParams: SwapParams) {
144+
const innerSwapParams = {
145+
...swapParams,
146+
receiver: swapParams.from,
147+
swapperMode: SwapperMode.EXACT_IN,
148+
isRepay: false,
149+
}
150+
151+
const quotes = await this.#binarySearchOverswapQuote(innerSwapParams)
152+
153+
if (!quotes) throw new Error("Quote not found")
154+
155+
return quotes.map((quote: SwapApiResponse) => {
156+
const multicallItems: SwapApiResponseMulticallItem[] = []
157+
158+
multicallItems.push(
159+
...quote.swap.multicallItems,
160+
...encodeRepayAndSweep(swapParams),
161+
)
162+
163+
const swap = buildApiResponseSwap(swapParams.from, multicallItems)
164+
165+
const verify = buildApiResponseVerifyDebtMax(
166+
swapParams.chainId,
167+
swapParams.receiver,
168+
swapParams.accountOut,
169+
swapParams.targetDebt,
170+
swapParams.deadline,
171+
)
172+
173+
return {
174+
amountIn: quote.amountIn,
175+
amountInMax: quote.amountInMax,
176+
amountOut: quote.amountOut,
177+
amountOutMin: quote.amountOutMin,
178+
vaultIn: swapParams.vaultIn,
179+
receiver: swapParams.receiver,
180+
accountIn: swapParams.accountIn,
181+
accountOut: swapParams.accountOut,
182+
tokenIn: swapParams.tokenIn,
183+
tokenOut: swapParams.tokenOut,
184+
slippage: swapParams.slippage,
185+
route: quote.route,
186+
swap,
187+
verify,
188+
}
189+
})
190+
}
191+
192+
async #binarySearchOverswapQuote(swapParams: SwapParams) {
193+
const swapParamsExactIn = {
194+
...swapParams,
195+
swapperMode: SwapperMode.EXACT_IN,
196+
receiver: swapParams.from,
197+
isRepay: false,
198+
}
199+
200+
const unitQuotes = await this.exactIn({
201+
...swapParamsExactIn,
202+
amount: parseUnits("1", swapParams.tokenIn.decimals),
203+
})
204+
205+
const unitAmountTo = unitQuotes[0].amountOutMin
206+
207+
const estimatedAmountIn = calculateEstimatedAmountFrom(
208+
BigInt(unitAmountTo),
209+
swapParamsExactIn.amount,
210+
swapParamsExactIn.tokenIn.decimals,
211+
swapParamsExactIn.tokenOut.decimals,
212+
)
213+
214+
if (estimatedAmountIn === 0n) throw new Error("quote not found")
215+
216+
const overSwapTarget = adjustForInterest(swapParams.amount)
217+
218+
const shouldContinue = (currentAmountTo: bigint): boolean =>
219+
// search until quote is 100 - 100.5% target
220+
currentAmountTo < overSwapTarget ||
221+
(currentAmountTo * 1000n) / overSwapTarget > 1005n
222+
223+
// single run to preselect sources
224+
const initialQuotes = await this.exactIn({
225+
...swapParams,
226+
amount: estimatedAmountIn,
227+
})
228+
229+
const allSettled = await Promise.allSettled(
230+
initialQuotes.map(async (initialQuote) =>
231+
promiseWithTimeout(async () => {
232+
const quote = await binarySearchQuote(
233+
swapParams,
234+
async (swapParams: SwapParams) => {
235+
const result = await this.exactIn(swapParams)
236+
return {
237+
quote: result[0],
238+
amountTo: BigInt(result[0].amountOutMin),
239+
}
240+
},
241+
overSwapTarget,
242+
estimatedAmountIn,
243+
shouldContinue,
244+
{
245+
quote: initialQuote,
246+
amountTo: BigInt(initialQuote.amountOutMin),
247+
},
248+
)
249+
return quote
250+
}, BINARY_SEARCH_TIMEOUT_SECONDS),
251+
),
252+
)
253+
254+
const bestQuotes = allSettled
255+
.filter((q) => q.status === "fulfilled")
256+
.map((q) => q.value)
257+
if (bestQuotes.length === 0) throw new Error("Quotes not found")
258+
259+
return bestQuotes
260+
}
261+
}

src/swapService/utils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,14 @@ export function matchParams(
9999
)
100100
return false
101101
}
102+
if (match.trades) {
103+
return match.trades.some((trade) => {
104+
return (
105+
isAddressEqual(trade.tokenIn, swapParams.tokenIn.addressInfo) &&
106+
isAddressEqual(trade.tokenOut, swapParams.tokenOut.addressInfo)
107+
)
108+
})
109+
}
102110

103111
return true
104112
}
@@ -505,6 +513,12 @@ export function encodeTargetDebtAsExactInMulticall(
505513
}),
506514
)
507515

516+
multicallItems.push(...encodeRepayAndSweep(swapParams))
517+
return multicallItems
518+
}
519+
520+
export function encodeRepayAndSweep(swapParams: SwapParams) {
521+
const multicallItems = []
508522
if (!swapParams.noRepayEncoding) {
509523
// FIXME - workaround for composite repay ERC4626 / over-swap
510524
const repayAmount =

0 commit comments

Comments
 (0)