Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/carlos/liquidity-manager' into E…
Browse files Browse the repository at this point in the history
…D-4570-retry-unfeasable
  • Loading branch information
StoyanD committed Jan 8, 2025
2 parents 005fb52 + e966ce0 commit 27784f3
Show file tree
Hide file tree
Showing 12 changed files with 255 additions and 1,332 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"@eco-foundation/routes-ts": "~0.2.10-beta",
"@launchdarkly/node-server-sdk": "^9.7.1",
"@liaoliaots/nestjs-redis-health": "^9.0.4",
"@lifi/sdk": "^3.4.1",
"@lifi/sdk": "^3.5.0",
"@nestjs/bullmq": "^10.1.1",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.2",
Expand All @@ -66,7 +66,7 @@
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"table": "^6.8.2",
"viem": "^2.21.32"
"viem": "^2.22.4"
},
"devDependencies": {
"@golevelup/ts-jest": "^0.5.0",
Expand Down
4 changes: 2 additions & 2 deletions src/common/errors/eco-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ export class EcoError extends Error {
}

// WatchIntent Service
static WatchEventUnsubscribeError = new Error('Could not unsubscribe from watch intent')
static WatchEventUnsubscribeError = new Error('Could not unsubscribe from watch event')
static WatchEventUnsubscribeFromError(chainID: number) {
return new Error(`Could not unsubscribe from watch intent for chain : ${chainID}`)
return new Error(`Could not unsubscribe from watch event for chain : ${chainID}`)
}
static WatchEventNoUnsubscribeError(chainID: number) {
return new Error(`There is no unwatch for chain : ${chainID}`)
Expand Down
1 change: 1 addition & 0 deletions src/intent/tests/utils-intent.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ describe('UtilsIntentService', () => {
targetsUnsupported: false,
selectorsUnsupported: false,
expiresEarly: false,
sameChainFulfill: false,
}
await utilsIntentService.updateInvalidIntentModel(model, invalidCause)
expect(mockUpdateOne).toHaveBeenCalledTimes(1)
Expand Down
31 changes: 29 additions & 2 deletions src/intent/tests/validate-intent.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,20 @@ describe('ValidateIntentService', () => {
expect(validateIntentService['validExpirationTime'](model)).toBe(false)
})
})

describe('on fulfillOnDifferentChain', () => {
it('should fail if the fulfillment is on the same chain as the event', async () => {
const model = { intent: { destinationChainID: 10 }, event: { sourceChainID: 10 } } as any
proofService.isIntentExpirationWithinProofMinimumDate.mockReturnValueOnce(true)
expect(validateIntentService['fulfillOnDifferentChain'](model)).toBe(false)
})

it('should succeed if the fulfillment is on a different chain as the event', async () => {
const model = { intent: { destinationChainID: 10 }, event: { sourceChainID: 20 } } as any
proofService.isIntentExpirationWithinProofMinimumDate.mockReturnValueOnce(true)
expect(validateIntentService['fulfillOnDifferentChain'](model)).toBe(true)
})
})
})

describe('on assertValidations', () => {
Expand All @@ -205,6 +219,7 @@ describe('ValidateIntentService', () => {
supportedTargets: 'targetsUnsupported',
supportedSelectors: 'selectorsUnsupported',
validExpirationTime: 'expiresEarly',
sameChainFulfill: 'sameChainFulfill',
}
beforeEach(() => {
utilsIntentService.updateInvalidIntentModel = updateInvalidIntentModel
Expand All @@ -215,7 +230,10 @@ describe('ValidateIntentService', () => {
})

it('should fail on equal rewards arrays', async () => {
const model = { intent: { rewardTokens: [1, 2], rewardAmounts: [3, 4, 5] } } as any
const model = {
intent: { rewardTokens: [1, 2], rewardAmounts: [3, 4, 5], destinationChainID: 10 },
event: { sourceChainID: 10 },
} as any
const solver = {} as any
expect(await validateIntentService['assertValidations'](model, solver)).toBe(false)
expect(mockLogLog).toHaveBeenCalledTimes(1)
Expand All @@ -228,13 +246,22 @@ describe('ValidateIntentService', () => {
entries(assetCases).forEach(([fun, boolVarName]: [string, string]) => {
it(`should fail on ${fun}`, async () => {
const model = {
intent: { rewardTokens: [1, 2], rewardAmounts: [3, 4], hash: '0x1' },
intent: {
rewardTokens: [1, 2],
rewardAmounts: [3, 4],
hash: '0x1',
destinationChainID: 10,
},
event: { sourceChainID: 11 },
} as any
const solver = {} as any
const logObj = entries(assetCases).reduce(
(ac, [, a]) => ({ ...ac, [a]: a == boolVarName }),
{},
)
if (boolVarName == 'sameChainFulfill') {
model.intent.destinationChainID = model.event.sourceChainID
}
const now = new Date()
proofService.getProofMinimumDate = jest.fn().mockReturnValueOnce(now)
validateIntentService[fun] = jest.fn().mockReturnValueOnce(false)
Expand Down
1 change: 1 addition & 0 deletions src/intent/utils-intent.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export class UtilsIntentService {
targetsUnsupported: boolean
selectorsUnsupported: boolean
expiresEarly: boolean
sameChainFulfill: boolean
},
) {
model.status = 'INVALID'
Expand Down
25 changes: 23 additions & 2 deletions src/intent/validate-intent.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { EcoError } from '../common/errors/eco-error'
* 2. Supports the targets
* 3. Supports the selectors
* 4. Has a valid expiration time
* 5. Fulfill chain not same as source chain
*
* As well as some structural checks on the intent model
*/
Expand Down Expand Up @@ -98,13 +99,21 @@ export class ValidateIntentService implements OnModuleInit {
const targetsUnsupported = !this.supportedTargets(model, solver)
const selectorsUnsupported = !this.supportedSelectors(model, solver)
const expiresEarly = !this.validExpirationTime(model)

if (proverUnsupported || targetsUnsupported || selectorsUnsupported || expiresEarly) {
const sameChainFulfill = !this.fulfillOnDifferentChain(model)

if (
proverUnsupported ||
targetsUnsupported ||
selectorsUnsupported ||
expiresEarly ||
sameChainFulfill
) {
await this.utilsIntentService.updateInvalidIntentModel(model, {
proverUnsupported,
targetsUnsupported,
selectorsUnsupported,
expiresEarly,
sameChainFulfill,
})
this.logger.log(
EcoLogMessage.fromDefault({
Expand All @@ -115,6 +124,7 @@ export class ValidateIntentService implements OnModuleInit {
targetsUnsupported,
selectorsUnsupported,
expiresEarly,
sameChainFulfill,
...(expiresEarly && {
proofMinDurationSeconds: this.proofService
.getProofMinimumDate(this.proofService.getProverType(model.intent.prover))
Expand Down Expand Up @@ -202,6 +212,17 @@ export class ValidateIntentService implements OnModuleInit {
)
}

/**
* Checks that the intent fulfillment is on a different chain than its source
* Needed since some proving methods(Hyperlane) cant prove same chain
* @param model the model of the source intent
* @param solver the solver used to fulfill
* @returns
*/
private fulfillOnDifferentChain(model: IntentSourceModel): boolean {
return model.intent.destinationChainID !== model.event.sourceChainID
}

/**
* Checks if the rewards and amounts arrays are of equal size
* @param model the source intent model
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
jest.mock('@lifi/sdk')

import { FlowProducer, Queue } from 'bullmq'
import { Test, TestingModule } from '@nestjs/testing'
import { BullModule, getFlowProducerToken, getQueueToken } from '@nestjs/bullmq'
import { createMock, DeepMocked } from '@golevelup/ts-jest'
import * as LiFi from '@lifi/sdk'
import { EcoConfigService } from '@/eco-configs/eco-config.service'
import { LiquidityManagerQueue } from '@/liquidity-manager/queues/liquidity-manager.queue'
import { LiFiProviderService } from '@/liquidity-manager/services/liquidity-providers/LiFi/lifi-provider.service'
import { KernelAccountClientV2Service } from '@/transaction/smart-wallets/kernel/kernel-account-client-v2.service'

describe('LiFiProviderService', () => {
let lifiProviderService: LiFiProviderService
let kernelAccountClientService: KernelAccountClientV2Service
let ecoConfigService: DeepMocked<EcoConfigService>
let queue: DeepMocked<Queue>
let flowProducer: DeepMocked<FlowProducer>

beforeEach(async () => {
const chainMod: TestingModule = await Test.createTestingModule({
providers: [
LiFiProviderService,
{ provide: EcoConfigService, useValue: createMock<EcoConfigService>() },
{
provide: KernelAccountClientV2Service,
useValue: createMock<KernelAccountClientV2Service>(),
},
],
imports: [
BullModule.registerQueue({ name: LiquidityManagerQueue.queueName }),
BullModule.registerFlowProducerAsync({ name: LiquidityManagerQueue.flowName }),
],
})
.overrideProvider(getQueueToken(LiquidityManagerQueue.queueName))
.useValue(createMock<Queue>())
.overrideProvider(getFlowProducerToken(LiquidityManagerQueue.flowName))
.useValue(createMock<FlowProducer>())
.compile()

ecoConfigService = chainMod.get(EcoConfigService)
lifiProviderService = chainMod.get(LiFiProviderService)
kernelAccountClientService = chainMod.get(KernelAccountClientV2Service)

queue = chainMod.get(getQueueToken(LiquidityManagerQueue.queueName))
flowProducer = chainMod.get(getFlowProducerToken(LiquidityManagerQueue.flowName))
})

const mockConfig = {
targetSlippage: 0.02,
intervalDuration: 1000,
thresholds: { surplus: 0.1, deficit: 0.2 },
}

afterEach(() => {
jest.restoreAllMocks()
})

describe('OnModuleInit', () => {
it('should configure LiFi SDK on init', async () => {
const mockGetClient = jest.spyOn(kernelAccountClientService, 'getClient')
mockGetClient.mockReturnValue({ account: { address: '0x123' } } as any)

jest.spyOn(ecoConfigService, 'getIntentSources').mockReturnValue([{ chainID: 10 }] as any)

const rpcUrls = { '10': 'http://op.rpc.com' }
jest.spyOn(ecoConfigService, 'getChainRPCs').mockReturnValue(rpcUrls)

await lifiProviderService.onModuleInit()

expect(mockGetClient).toHaveBeenCalled()
expect(lifiProviderService['walletAddress']).toEqual('0x123')
expect(LiFi.createConfig).toHaveBeenCalledWith(
expect.objectContaining({
integrator: 'Eco',
rpcUrls: { '10': [rpcUrls['10']] },
}),
)
})
})

describe('getQuote', () => {
it('should return a quote', async () => {
const mockTokenIn = {
chainId: 1,
config: { address: '0xTokenIn' },
balance: { decimals: 18 },
}
const mockTokenOut = {
chainId: 1,
config: { address: '0xTokenOut' },
balance: { decimals: 18 },
}
const mockRoute = {
fromAmount: '1000000000000000000',
toAmount: '2000000000000000000',
toAmountMin: '1900000000000000000',
steps: [],
}
jest.spyOn(LiFi, 'getRoutes').mockResolvedValue({ routes: [mockRoute] } as any)

const result = await lifiProviderService.getQuote(mockTokenIn as any, mockTokenOut as any, 1)

expect(result.amountIn).toEqual(BigInt(mockRoute.fromAmount))
expect(result.amountOut).toEqual(BigInt(mockRoute.toAmount))
expect(result.slippage).toBeCloseTo(0.05)
expect(result.tokenIn).toEqual(mockTokenIn)
expect(result.tokenOut).toEqual(mockTokenOut)
expect(result.strategy).toEqual('LiFi')
expect(result.context).toEqual(mockRoute)
})
})

describe('execute', () => {
it('should execute a quote', async () => {
const mockQuote = {
tokenIn: { config: { address: '0xTokenIn', chainId: 1 } },
tokenOut: { config: { address: '0xTokenOut', chainId: 1 } },
amountIn: BigInt(1000000000000000000),
amountOut: BigInt(2000000000000000000),
slippage: 0.05,
context: { gasCostUSD: 10, steps: [] },
}
const mockExecuteRoute = jest.spyOn(LiFi, 'executeRoute')

await lifiProviderService.execute(mockQuote as any)

expect(mockExecuteRoute).toHaveBeenCalledWith(mockQuote.context, expect.any(Object))
})
})
})
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { Injectable, Logger, OnModuleInit } from '@nestjs/common'
import { parseUnits } from 'viem'
import { createConfig, executeRoute, getRoutes, RoutesRequest, SDKConfig } from '@lifi/sdk'
import { createConfig, EVM, executeRoute, getRoutes, RoutesRequest, SDKConfig } from '@lifi/sdk'
import { EcoLogMessage } from '@/common/logging/eco-log-message'
import { EcoConfigService } from '@/eco-configs/eco-config.service'
import { KernelAccountClientV2Service } from '@/transaction/smart-wallets/kernel/kernel-account-client-v2.service'
import { customEVM } from '@/liquidity-manager/services/liquidity-providers/LiFi/providers/evm/custom-evm'
import { logLiFiProcess } from '@/liquidity-manager/services/liquidity-providers/LiFi/utils/get-transaction-hashes'
import { KernelAccountClientV2Service } from '@/transaction/smart-wallets/kernel/kernel-account-client-v2.service'

@Injectable()
export class LiFiProviderService implements OnModuleInit {
Expand All @@ -29,7 +28,7 @@ export class LiFiProviderService implements OnModuleInit {
integrator: 'Eco',
rpcUrls: this.getLiFiRPCUrls(),
providers: [
customEVM({
EVM({
getWalletClient: () => Promise.resolve(client),
switchChain: (chainId) => this.kernelAccountClientService.getClient(chainId),
}),
Expand Down
Loading

0 comments on commit 27784f3

Please sign in to comment.