HOME
API
LAUNCH APP

Aperture’s Intents API

Institutions and advanced users can go beyond Aperture’s front-end with our intents API for enhanced, declarative strategies that fit into your existing workflow.


import {
  Action,
  ActionTypeEnum,
  AutomanFragment,
  ApertureSupportedChainId,
  IUniV3Automan__factory,
  PermitInfo,
  RebalanceAction,
  UniV3Automan,
  UniV3Automan__factory,
  getAutomanDecreaseLiquidityCallInfo,
  getAutomanRebalanceCallInfo,
  getAutomanReinvestCallInfo,
  getChainInfo,
  normalizeTicks,
  optimalRebalance,
} from '@aperture_finance/uniswap-v3-automation-sdk';
import { BlockTag, JsonRpcProvider, Provider } from '@ethersproject/providers';
import { Percent } from '@uniswap/sdk-core';
import { Position } from '@uniswap/v3-sdk';
import Big from 'big.js';
import {
  BigNumber,
  BigNumberish,
  PopulatedTransaction,
  Signer,
  ethers,
} from 'ethers';
import JSBI from 'jsbi';

import { GAS_LIMIT_L2_MULTIPLIER, GAS_LIMIT_MULTIPLIER } from '../../constants';
import { getChainIdFromPrimaryKey, isRecurringAction } from '../../db_helper';
import { StateTableItem } from '../../interfaces/schema';
import { logger } from '../../logger';

function isRebalanceAction(action: Action): boolean {
  return (
    action.type === ActionTypeEnum.enum.Rebalance || isRecurringAction(action)
  );
}

export async function populateTriggerActionTx(
  task: StateTableItem,
  position: Position,
  positionRawEtherValue: Big,
  permitInfo: PermitInfo | undefined,
  signer: Signer,
  provider: JsonRpcProvider | Provider,
  blockNumber?: number,
) {
  const chainId = getChainIdFromPrimaryKey(task.primary_key);
  const signerAddress = await signer.getAddress();
  const automanContract = UniV3Automan__factory.connect(
    getChainInfo(chainId).aperture_uniswap_v3_automan,
    signer,
  );

  // Find the gas deduction ratio and compare with user-specified ceiling.
  let gasUnits: BigNumber;
  let gasDeductionPips: BigNumber;
  let amount0Min: JSBI;
  let amount1Min: JSBI;
  let swapData: string | undefined;
  if (isRebalanceAction(task.action)) {
    // get swap data using 1inch
    swapData = await get1inchSwapData(
      position,
      task,
      /*feePips=*/ 0,
      signerAddress,
      provider,
      blockNumber,
    );
    logger.info(`Using 1inch swap data: ${swapData}`);
  }
  if (swapData !== undefined) {
    const [poolEstimate, routerEstimate] = await Promise.all([
      estimateGasAndSimulate(
        task,
        position,
        positionRawEtherValue,
        automanContract,
        signerAddress,
        permitInfo,
        /**swapData= */ undefined,
        blockNumber,
      ),
      estimateGasAndSimulate(
        task,
        position,
        positionRawEtherValue,
        automanContract,
        signerAddress,
        permitInfo,
        swapData,
        blockNumber,
      ),
    ]);
    logger.info(
      `Pool estimate: ${poolEstimate.liquidity!.toString()}, gas: ${poolEstimate.gasUnits.toString()}`,
    );
    logger.info(
      `Router estimate: ${routerEstimate.liquidity!.toString()}, gas: ${routerEstimate.gasUnits.toString()}`,
    );
    if (poolEstimate.liquidity!.gte(routerEstimate.liquidity!)) {
      ({ gasUnits, gasDeductionPips, amount0Min, amount1Min } = poolEstimate);
      // Clear swap data since we are using the pool to rebalance; otherwise, the populated tx would still use 1inch.
      swapData = undefined;
      logger.info(`Use the same pool to rebalance`);
    } else {
      ({ gasUnits, gasDeductionPips, amount0Min, amount1Min } = routerEstimate);
      logger.info(`Use 1inch to rebalance`);
    }
  } else {
    ({ gasUnits, gasDeductionPips, amount0Min, amount1Min } =
      await estimateGasAndSimulate(
        task,
        position,
        positionRawEtherValue,
        automanContract,
        signerAddress,
        permitInfo,
        /*swapData=*/ undefined,
        blockNumber,
      ));
  }

  // Execute action.
  const populatedTx = await generateTxPromise(
    position,
    task,
    permitInfo,
    automanContract,
    /*mode=*/ 'populate',
    amount0Min.toString(),
    amount1Min.toString(),
    /*feePips=*/ gasDeductionPips,
    signerAddress,
    swapData,
    blockNumber,
  );
  // Populating gas limit based on estimated gas units multiplied by a multiplier.
  populatedTx.gasLimit = gasUnits.mul(GAS_LIMIT_MULTIPLIER).div(100);
  return populatedTx;
}

async function get1inchSwapData(
  position: Position,
  task: StateTableItem,
  feePips: BigNumberish,
  signerAddress: string,
  provider: JsonRpcProvider | Provider,
  blockNumber?: number,
) {
  const { tickLower, tickUpper } = normalizeTicks(task.action, position.pool);
  try {
    const { swapData } = await optimalRebalance(
      position.pool.chainId,
      task.nft_id,
      tickLower,
      tickUpper,
      feePips,
      false,
      signerAddress,
      (task.action as RebalanceAction).slippage,
      provider,
      blockNumber,
    );
    return swapData;
  } catch (err) {
    logger.error(`Failed to get 1inch swap data: ${err}`);
  }
}

async function estimateGasAndSimulate(
  task: StateTableItem,
  position: Position,
  positionRawEtherValue: Big,
  automanContract: UniV3Automan,
  signerAddress: string,
  permitInfo?: PermitInfo,
  swapData = '0x',
  blockTag: BlockTag = 'latest',
) {
  const { gasUnits, gasDeductionPips } = await estimateGasDeductionPips(
    positionRawEtherValue,
    position,
    task,
    permitInfo,
    automanContract,
    signerAddress,
    getChainIdFromPrimaryKey(task.primary_key),
    swapData,
    blockTag,
  );
  const amounts = await getAmountsAfterSlippage(
    position,
    task,
    permitInfo,
    automanContract,
    signerAddress,
    gasDeductionPips,
    swapData,
    blockTag,
  );
  return {
    ...amounts,
    gasUnits,
    gasDeductionPips,
  };
}

type Mode = {
  estimateGas: BigNumber;
  callStatic: { liquidity?: BigNumber; amount0: BigNumber; amount1: BigNumber };
  populate: PopulatedTransaction;
};

// Generate tx that triggers `action`.
// If `mode` is `estimateGas`, then a Promise<BigNumber> is returned representing the estimated tx cost.
// If `mode` is `callStatic`, then a Promise<any> is returned with the return value of the invoked function.
// Otherwise, a Promise<TransactionResponse> is returned with the response for the tx after it is sent to the network.
async function generateTxPromise<K extends keyof Mode>(
  position: Position,
  task: StateTableItem,
  permitInfo: PermitInfo | undefined,
  automanContract: UniV3Automan,
  mode: K,
  amount0Min: BigNumberish,
  amount1Min: BigNumberish,
  feePips: BigNumberish,
  signerAddress: string,
  swapData = '0x',
  blockTag?: BlockTag,
): Promise<Mode[K]> {
  const deadline = Math.floor(Date.now() / 1000) + 20 * 60; // Twenty min from now.
  let functionFragment: AutomanFragment;
  let data: string;
  switch (task.action.type) {
    case 'Close':
    case 'LimitOrderClose': {
      ({ functionFragment, data } = getAutomanDecreaseLiquidityCallInfo(
        task.nft_id,
        position.liquidity.toString(),
        deadline,
        amount0Min,
        amount1Min,
        feePips,
        permitInfo,
      ));
      break;
    }
    case 'Reinvest': {
      ({ functionFragment, data } = getAutomanReinvestCallInfo(
        task.nft_id,
        deadline,
        amount0Min,
        amount1Min,
        feePips,
        permitInfo,
      ));
      break;
    }
    case 'Rebalance':
    case 'RecurringPercentage':
    case 'RecurringPrice':
    case 'RecurringRatio': {
      const pool = position.pool;
      const { tickLower, tickUpper } = normalizeTicks(task.action, pool);
      if (
        tickLower === position.tickLower &&
        tickUpper === position.tickUpper
      ) {
        throw new Error('No need to rebalance');
      }

      ({ functionFragment, data } = getAutomanRebalanceCallInfo(
        {
          token0: pool.token0.address,
          token1: pool.token1.address,
          fee: pool.fee,
          tickLower,
          tickUpper,
          amount0Desired: 0, // Param value ignored by Automan.
          amount1Desired: 0, // Param value ignored by Automan.
          amount0Min: amount0Min,
          amount1Min: amount1Min,
          recipient: ethers.constants.AddressZero, // Param value ignored by Automan.
          deadline: deadline,
        },
        task.nft_id,
        feePips,
        permitInfo,
        swapData,
      ));
      break;
    }
    default:
      throw new Error('Invalid action type');
  }
  switch (mode) {
    case 'estimateGas':
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      return automanContract.provider.estimateGas({
        from: signerAddress,
        to: automanContract.address,
        data,
      });
    case 'callStatic':
      const returnData = await automanContract.provider.call(
        {
          from: signerAddress,
          to: automanContract.address,
          data,
        },
        blockTag,
      );
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      return IUniV3Automan__factory.createInterface().decodeFunctionResult(
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        functionFragment,
        returnData,
      );
    default:
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      return automanContract.signer.populateTransaction({
        to: automanContract.address,
        data,
      });
  }
}
      

automated position rebalancing, closure & compounding

We offer support on various DeFi strategies, including active liquidity management — automated position rebalancing, closure, and compounding. You can choose among JavaScript, Python, Rust, and other languages of your choice to leverage this arsenal of automation features.