UNPKG

@lifi/sdk

Version:

LI.FI Any-to-Any Cross-Chain-Swap SDK

219 lines (192 loc) 6.57 kB
import type { Route } from '@lifi/types' import { config } from '../config.js' import { LiFiErrorCode } from '../errors/constants.js' import { ProviderError } from '../errors/errors.js' import { executionState } from './executionState.js' import { prepareRestart } from './prepareRestart.js' import type { ExecutionOptions, RouteExtended } from './types.js' /** * Execute a route. * @param route - The route that should be executed. Cannot be an active route. * @param executionOptions - An object containing settings and callbacks. * @returns The executed route. * @throws {LiFiError} Throws a LiFiError if the execution fails. */ export const executeRoute = async ( route: Route, executionOptions?: ExecutionOptions ): Promise<RouteExtended> => { // Deep clone to prevent side effects const clonedRoute = structuredClone<Route>(route) let executionPromise = executionState.get(clonedRoute.id)?.promise // Check if route is already running if (executionPromise) { return executionPromise } executionState.create({ route: clonedRoute, executionOptions }) executionPromise = executeSteps(clonedRoute) executionState.update({ route: clonedRoute, promise: executionPromise, }) return executionPromise } /** * Resume the execution of a route that has been stopped or had an error while executing. * @param route - The route that is to be executed. Cannot be an active route. * @param executionOptions - An object containing settings and callbacks. * @returns The executed route. * @throws {LiFiError} Throws a LiFiError if the execution fails. */ export const resumeRoute = async ( route: Route, executionOptions?: ExecutionOptions ): Promise<RouteExtended> => { const execution = executionState.get(route.id) if (execution) { const executionHalted = execution.executors.some( (executor) => !executor.allowExecution ) if (!executionHalted) { // Check if we want to resume route execution in the background updateRouteExecution(route, { executeInBackground: executionOptions?.executeInBackground, }) if (!execution.promise) { // We should never reach this point if we do clean-up properly throw new Error('Route execution promise not found.') } return execution.promise } } prepareRestart(route) return executeRoute(route, executionOptions) } const executeSteps = async (route: RouteExtended): Promise<RouteExtended> => { // Loop over steps and execute them for (let index = 0; index < route.steps.length; index++) { const execution = executionState.get(route.id) // Check if execution has stopped in the meantime if (!execution) { break } const step = route.steps[index] const previousStep = route.steps[index - 1] // Check if the step is already done // if (step.execution?.status === 'DONE') { continue } // Update step fromAmount using output of the previous step execution. In the future this should be handled by calling `updateRoute` if (previousStep?.execution?.toAmount) { step.action.fromAmount = previousStep.execution.toAmount if (step.includedSteps?.length) { step.includedSteps[0].action.fromAmount = previousStep.execution.toAmount } } try { const fromAddress = step.action.fromAddress if (!fromAddress) { throw new Error('Action fromAddress is not specified.') } const provider = config .get() .providers.find((provider) => provider.isAddress(fromAddress)) if (!provider) { throw new ProviderError( LiFiErrorCode.ProviderUnavailable, 'SDK Execution Provider not found.' ) } const stepExecutor = await provider.getStepExecutor({ routeId: route.id, executionOptions: execution.executionOptions, }) execution.executors.push(stepExecutor) // Check if we want to execute this step in the background if (execution.executionOptions) { updateRouteExecution(route, execution.executionOptions) } const executedStep = await stepExecutor.executeStep(step) // We may reach this point if user interaction isn't allowed. We want to stop execution until we resume it if (executedStep.execution?.status !== 'DONE') { stopRouteExecution(route) } // Execution stopped during the current step, we don't want to continue to the next step so we return already if (!stepExecutor.allowExecution) { return route } } catch (e) { stopRouteExecution(route) throw e } } // Clean up after the execution executionState.delete(route.id) return route } /** * Updates route execution to background or foreground state. * @param route - A route that is currently in execution. * @param options - An object with execution settings. */ export const updateRouteExecution = ( route: Route, options: ExecutionOptions ): void => { const execution = executionState.get(route.id) if (!execution) { return } if ('executeInBackground' in options) { for (const executor of execution.executors) { executor.setInteraction({ allowInteraction: !options?.executeInBackground, allowUpdates: true, }) } } // Update active route settings so we know what the current state of execution is execution.executionOptions = { ...execution.executionOptions, ...options, } } /** * Stops the execution of an active route. * @param route - A route that is currently in execution. * @returns The stopped route. */ export const stopRouteExecution = (route: Route): Route => { const execution = executionState.get(route.id) if (!execution) { return route } for (const executor of execution.executors) { executor.setInteraction({ allowInteraction: false, allowUpdates: false, allowExecution: false, }) } executionState.delete(route.id) return execution.route } /** * Get the list of active routes. * @returns A list of routes. */ export const getActiveRoutes = (): RouteExtended[] => { return Object.values(executionState.state) .map((dict) => dict?.route) .filter(Boolean) as RouteExtended[] } /** * Return the current route information for given route. The route has to be active. * @param routeId - A route id. * @returns The updated route. */ export const getActiveRoute = (routeId: string): RouteExtended | undefined => { return executionState.get(routeId)?.route }