diff --git a/prime/src/components/borrow/actions/AloeAddMarginActionCard.tsx b/prime/src/components/borrow/actions/AloeAddMarginActionCard.tsx index 0d8fc8026..3f50d944c 100644 --- a/prime/src/components/borrow/actions/AloeAddMarginActionCard.tsx +++ b/prime/src/components/borrow/actions/AloeAddMarginActionCard.tsx @@ -4,6 +4,7 @@ import { Dropdown, DropdownOption } from 'shared/lib/components/common/Dropdown' import { getTransferInActionArgs } from '../../../data/actions/ActionArgs'; import { ActionID } from '../../../data/actions/ActionID'; +import { transferInOperator } from '../../../data/actions/ActionOperators'; import { ActionCardProps, ActionProviders, @@ -67,6 +68,10 @@ export function AloeAddMarginActionCard(prop: ActionCardProps) { selectedToken: selectedToken, }, uniswapResult: null, + operator(operand) { + if (!operand || selectedToken == null) return null; + return transferInOperator(operand, selectedToken, parsedValue); + }, }); }; @@ -95,12 +100,15 @@ export function AloeAddMarginActionCard(prop: ActionCardProps) { { + onSelect={(option: DropdownOption) => { if (option.value !== selectedTokenOption.value) { onChange({ actionId: ActionID.TRANSFER_IN, aloeResult: { selectedToken: parseSelectedToken(option.value) }, uniswapResult: null, + operator(operand) { + return null; + }, }); } }} diff --git a/prime/src/components/borrow/actions/AloeBorrowActionCard.tsx b/prime/src/components/borrow/actions/AloeBorrowActionCard.tsx index 77d641882..80357cf3e 100644 --- a/prime/src/components/borrow/actions/AloeBorrowActionCard.tsx +++ b/prime/src/components/borrow/actions/AloeBorrowActionCard.tsx @@ -4,6 +4,7 @@ import { Dropdown, DropdownOption } from 'shared/lib/components/common/Dropdown' import { getBorrowActionArgs } from '../../../data/actions/ActionArgs'; import { ActionID } from '../../../data/actions/ActionID'; +import { borrowOperator } from '../../../data/actions/ActionOperators'; import { ActionCardProps, ActionProviders, @@ -56,6 +57,10 @@ export function AloeBorrowActionCard(prop: ActionCardProps) { selectedToken: selectedToken, }, uniswapResult: null, + operator(operand) { + if (!operand || selectedToken == null) return null; + return borrowOperator(operand, selectedToken, Math.max(amount0, amount1)); + }, }); }; @@ -75,12 +80,15 @@ export function AloeBorrowActionCard(prop: ActionCardProps) { { + onSelect={(option: DropdownOption) => { if (option.value !== selectedTokenOption.value) { onChange({ actionId: ActionID.BORROW, aloeResult: { selectedToken: parseSelectedToken(option.value) }, uniswapResult: null, + operator(operand) { + return null; + }, }); } }} diff --git a/prime/src/components/borrow/actions/AloeBurnTokenPlusActionCard.tsx b/prime/src/components/borrow/actions/AloeBurnTokenPlusActionCard.tsx index 7701d8a45..0b49f5347 100644 --- a/prime/src/components/borrow/actions/AloeBurnTokenPlusActionCard.tsx +++ b/prime/src/components/borrow/actions/AloeBurnTokenPlusActionCard.tsx @@ -4,6 +4,7 @@ import { Dropdown, DropdownOption } from 'shared/lib/components/common/Dropdown' import { getBurnActionArgs } from '../../../data/actions/ActionArgs'; import { ActionID } from '../../../data/actions/ActionID'; +import { burnOperator } from '../../../data/actions/ActionOperators'; import { ActionCardProps, ActionProviders, @@ -56,6 +57,10 @@ export function AloeBurnTokenPlusActionCard(prop: ActionCardProps) { selectedToken: selectedToken, }, uniswapResult: null, + operator(operand) { + if (!operand || selectedToken == null) return null; + return burnOperator(operand, selectedToken, parsedValue); + }, }); }; @@ -77,12 +82,15 @@ export function AloeBurnTokenPlusActionCard(prop: ActionCardProps) { { + onSelect={(option: DropdownOption) => { if (option.value !== selectedTokenOption.value) { onChange({ actionId: ActionID.BURN, aloeResult: { selectedToken: parseSelectedToken(option.value) }, uniswapResult: null, + operator(operand) { + return null; + }, }); } }} diff --git a/prime/src/components/borrow/actions/AloeMintTokenPlusActionCard.tsx b/prime/src/components/borrow/actions/AloeMintTokenPlusActionCard.tsx index 11a202eef..744cb992c 100644 --- a/prime/src/components/borrow/actions/AloeMintTokenPlusActionCard.tsx +++ b/prime/src/components/borrow/actions/AloeMintTokenPlusActionCard.tsx @@ -4,6 +4,7 @@ import { Dropdown, DropdownOption } from 'shared/lib/components/common/Dropdown' import { getMintActionArgs } from '../../../data/actions/ActionArgs'; import { ActionID } from '../../../data/actions/ActionID'; +import { mintOperator } from '../../../data/actions/ActionOperators'; import { ActionCardProps, ActionProviders, @@ -55,6 +56,10 @@ export function AloeMintTokenPlusActionCard(prop: ActionCardProps) { selectedToken: selectedToken, }, uniswapResult: null, + operator(operand) { + if (!operand || selectedToken == null) return null; + return mintOperator(operand, selectedToken, parsedValue); + }, }); }; @@ -76,12 +81,15 @@ export function AloeMintTokenPlusActionCard(prop: ActionCardProps) { { + onSelect={(option: DropdownOption) => { if (option.value !== selectedTokenOption.value) { onChange({ actionId: ActionID.MINT, aloeResult: { selectedToken: parseSelectedToken(option.value) }, uniswapResult: null, + operator(operand) { + return null; + }, }); } }} diff --git a/prime/src/components/borrow/actions/AloeRepayActionCard.tsx b/prime/src/components/borrow/actions/AloeRepayActionCard.tsx index 3ba3bae7d..39d20308f 100644 --- a/prime/src/components/borrow/actions/AloeRepayActionCard.tsx +++ b/prime/src/components/borrow/actions/AloeRepayActionCard.tsx @@ -4,6 +4,7 @@ import { Dropdown, DropdownOption } from 'shared/lib/components/common/Dropdown' import { getRepayActionArgs } from '../../../data/actions/ActionArgs'; import { ActionID } from '../../../data/actions/ActionID'; +import { repayOperator } from '../../../data/actions/ActionOperators'; import { ActionCardProps, ActionProviders, @@ -56,6 +57,10 @@ export function AloeRepayActionCard(prop: ActionCardProps) { selectedToken: selectedToken, }, uniswapResult: null, + operator(operand) { + if (!operand || selectedToken == null) return null; + return repayOperator(operand, selectedToken, Math.max(amount0, amount1)); + }, }); }; @@ -78,12 +83,15 @@ export function AloeRepayActionCard(prop: ActionCardProps) { { + onSelect={(option: DropdownOption) => { if (option.value !== selectedTokenOption.value) { onChange({ actionId: ActionID.REPAY, aloeResult: { selectedToken: parseSelectedToken(option.value) }, uniswapResult: null, + operator(operand) { + return null; + }, }); } }} diff --git a/prime/src/components/borrow/actions/AloeWithdrawActionCard.tsx b/prime/src/components/borrow/actions/AloeWithdrawActionCard.tsx index 17480aa7f..fdaafe7f5 100644 --- a/prime/src/components/borrow/actions/AloeWithdrawActionCard.tsx +++ b/prime/src/components/borrow/actions/AloeWithdrawActionCard.tsx @@ -4,6 +4,7 @@ import { Dropdown, DropdownOption } from 'shared/lib/components/common/Dropdown' import { getTransferOutActionArgs } from '../../../data/actions/ActionArgs'; import { ActionID } from '../../../data/actions/ActionID'; +import { transferOutOperator } from '../../../data/actions/ActionOperators'; import { ActionCardProps, ActionProviders, @@ -66,6 +67,10 @@ export function AloeWithdrawActionCard(prop: ActionCardProps) { selectedToken: selectedToken, }, uniswapResult: null, + operator(operand) { + if (!operand || selectedToken == null) return null; + return transferOutOperator(operand, selectedToken, parsedValue); + }, }); }; @@ -85,12 +90,15 @@ export function AloeWithdrawActionCard(prop: ActionCardProps) { { + onSelect={(option: DropdownOption) => { if (option.value !== selectedTokenOption.value) { onChange({ actionId: ActionID.TRANSFER_OUT, aloeResult: { selectedToken: parseSelectedToken(option.value) }, uniswapResult: null, + operator(operand) { + return null; + }, }); } }} diff --git a/prime/src/components/borrow/actions/UniswapAddLiquidityActionCard.tsx b/prime/src/components/borrow/actions/UniswapAddLiquidityActionCard.tsx index 51ff12d05..55059d00b 100644 --- a/prime/src/components/borrow/actions/UniswapAddLiquidityActionCard.tsx +++ b/prime/src/components/borrow/actions/UniswapAddLiquidityActionCard.tsx @@ -2,10 +2,11 @@ import { useEffect, useState } from 'react'; import { TickMath } from '@uniswap/v3-sdk'; import JSBI from 'jsbi'; -import { useProvider } from 'wagmi'; +import { Address, useProvider } from 'wagmi'; import { getAddLiquidityActionArgs } from '../../../data/actions/ActionArgs'; import { ActionID } from '../../../data/actions/ActionID'; +import { addLiquidityOperator } from '../../../data/actions/ActionOperators'; import { ActionCardProps, ActionProviders } from '../../../data/actions/Actions'; import useEffectOnce from '../../../data/hooks/UseEffectOnce'; import { roundDownToNearestN, roundUpToNearestN } from '../../../util/Numbers'; @@ -257,6 +258,19 @@ export default function UniswapAddLiquidityActionCard(props: ActionCardProps) { isAmount0LastUpdated: localIsAmount0UserDefined, isToken0Selected: isToken0Selected, }, + operator(operand) { + if (!operand || lowerTick == null || upperTick == null || currentTick == null) return null; + return addLiquidityOperator( + operand, + marginAccount.address as Address, + liquidity, + lowerTick, + upperTick, + currentTick, + token0.decimals, + token1.decimals + ); + }, }); } diff --git a/prime/src/components/borrow/actions/UniswapClaimFeesActionCard.tsx b/prime/src/components/borrow/actions/UniswapClaimFeesActionCard.tsx index 3fc21e106..67929f5d6 100644 --- a/prime/src/components/borrow/actions/UniswapClaimFeesActionCard.tsx +++ b/prime/src/components/borrow/actions/UniswapClaimFeesActionCard.tsx @@ -2,11 +2,14 @@ import JSBI from 'jsbi'; import { DropdownOption, DropdownWithPlaceholder } from 'shared/lib/components/common/Dropdown'; import { Text } from 'shared/lib/components/common/Typography'; import styled from 'styled-components'; +import { Address } from 'wagmi'; import { ReactComponent as InboxIcon } from '../../../assets/svg/inbox.svg'; import { getRemoveLiquidityActionArgs } from '../../../data/actions/ActionArgs'; import { ActionID } from '../../../data/actions/ActionID'; +import { removeLiquidityOperator } from '../../../data/actions/ActionOperators'; import { ActionCardProps, ActionProviders, UniswapPosition } from '../../../data/actions/Actions'; +import { sqrtRatioToTick } from '../../../util/Uniswap'; import { BaseActionCard } from '../BaseActionCard'; //TOOD: merge this with the existing UniswapPosition? @@ -29,7 +32,8 @@ const SVGIconWrapper = styled.div.attrs((props: { width: number; height: number `; export default function UnsiwapClaimFeesActionCard(props: ActionCardProps) { - const { uniswapPositions, previousActionCardState, isCausingError, onChange, onRemove } = props; + const { marginAccount, uniswapPositions, previousActionCardState, isCausingError, onChange, onRemove } = props; + const { token0, token1 } = marginAccount; const dropdownOptions = uniswapPositions.map((lp, index) => { return { @@ -72,6 +76,19 @@ export default function UnsiwapClaimFeesActionCard(props: ActionCardProps) { isAmount0LastUpdated: undefined, isToken0Selected: undefined, }, + operator(operand) { + if (!operand || lower == null || upper == null) return null; + return removeLiquidityOperator( + operand, + marginAccount.address as Address, + updatedLiquidity, + lower, + upper, + sqrtRatioToTick(marginAccount.sqrtPriceX96), + token0.decimals, + token1.decimals + ); + }, }); } diff --git a/prime/src/components/borrow/actions/UniswapRemoveLiquidityActionCard.tsx b/prime/src/components/borrow/actions/UniswapRemoveLiquidityActionCard.tsx index d9f67f556..6c2d9ed53 100644 --- a/prime/src/components/borrow/actions/UniswapRemoveLiquidityActionCard.tsx +++ b/prime/src/components/borrow/actions/UniswapRemoveLiquidityActionCard.tsx @@ -5,11 +5,13 @@ import { DropdownOption, DropdownWithPlaceholder } from 'shared/lib/components/c import { SquareInputWithTrailingUnit } from 'shared/lib/components/common/Input'; import { Text } from 'shared/lib/components/common/Typography'; import styled from 'styled-components'; +import { Address } from 'wagmi'; import { ReactComponent as InboxIcon } from '../../../assets/svg/inbox.svg'; import { ReactComponent as RightArrowIcon } from '../../../assets/svg/small_right_arrow.svg'; import { getRemoveLiquidityActionArgs } from '../../../data/actions/ActionArgs'; import { ActionID } from '../../../data/actions/ActionID'; +import { removeLiquidityOperator } from '../../../data/actions/ActionOperators'; import { ActionCardProps, ActionProviders, UniswapPosition } from '../../../data/actions/Actions'; import useEffectOnce from '../../../data/hooks/UseEffectOnce'; import { formatNumberInput, formatTokenAmount } from '../../../util/Numbers'; @@ -137,6 +139,19 @@ export default function UniswapRemoveLiquidityActionCard(props: ActionCardProps) isAmount0LastUpdated: undefined, isToken0Selected: undefined, }, + operator(operand) { + if (!operand || lower == null || upper == null) return null; + return removeLiquidityOperator( + operand, + marginAccount.address as Address, + liquidityToRemove, + lower, + upper, + sqrtRatioToTick(marginAccount.sqrtPriceX96), + token0.decimals, + token1.decimals + ); + }, }); } diff --git a/prime/src/data/actions/ActionOperators.ts b/prime/src/data/actions/ActionOperators.ts new file mode 100644 index 000000000..b218056eb --- /dev/null +++ b/prime/src/data/actions/ActionOperators.ts @@ -0,0 +1,164 @@ +import JSBI from 'jsbi'; +import { Address } from 'wagmi'; + +import { getAmountsForLiquidity, uniswapPositionKey } from '../../util/Uniswap'; +import { ActionCardOperand, TokenType } from './Actions'; + +export function transferInOperator(operand: ActionCardOperand, token: TokenType, amount: number): ActionCardOperand { + const assets = { ...operand.assets }; + const availableBalances = { ...operand.availableBalances }; + const requiredAllowances = { ...operand.requiredAllowances }; + + switch (token) { + case TokenType.ASSET0: + assets.token0Raw += amount; + availableBalances.amount0Asset -= amount; + requiredAllowances.amount0Asset += amount; + break; + case TokenType.ASSET1: + assets.token1Raw += amount; + availableBalances.amount1Asset -= amount; + requiredAllowances.amount1Asset += amount; + break; + case TokenType.KITTY0: + assets.token0Plus += amount; + availableBalances.amount0Kitty -= amount; + requiredAllowances.amount0Kitty += amount; + break; + case TokenType.KITTY1: + assets.token1Plus += amount; + availableBalances.amount1Kitty -= amount; + requiredAllowances.amount1Kitty += amount; + break; + } + + return { ...operand, assets, availableBalances }; +} + +export function transferOutOperator(operand: ActionCardOperand, token: TokenType, amount: number): ActionCardOperand { + return transferInOperator(operand, token, -amount); +} + +export function mintOperator(operand: ActionCardOperand, token: TokenType, amount: number): ActionCardOperand { + const assets = { ...operand.assets }; + + if (token === TokenType.ASSET0) { + assets.token0Raw -= amount; + assets.token0Plus += amount; + } else { + assets.token1Raw -= amount; + assets.token1Plus += amount; + } + + return { ...operand, assets }; +} + +export function burnOperator(operand: ActionCardOperand, token: TokenType, amount: number): ActionCardOperand { + return mintOperator(operand, token, -amount); +} + +export function borrowOperator(operand: ActionCardOperand, token: TokenType, amount: number): ActionCardOperand { + const assets = { ...operand.assets }; + const liabilities = { ...operand.liabilities }; + + if (token === TokenType.ASSET0) { + assets.token0Raw += amount; + liabilities.amount0 += amount; + } else { + assets.token1Raw += amount; + liabilities.amount1 += amount; + } + + return { ...operand, assets, liabilities }; +} + +export function repayOperator(operand: ActionCardOperand, token: TokenType, amount: number): ActionCardOperand { + return borrowOperator(operand, token, -amount); +} + +export function addLiquidityOperator( + operand: ActionCardOperand, + owner: Address, + liquidity: JSBI, + lowerTick: number, + upperTick: number, + currentTick: number, + token0Decimals: number, + token1Decimals: number +): ActionCardOperand { + const assets = { ...operand.assets }; + const uniswapPositions = operand.uniswapPositions.concat(); + + const [amount0, amount1] = getAmountsForLiquidity( + liquidity, + lowerTick, + upperTick, + currentTick, + token0Decimals, + token1Decimals + ); + + assets.token0Raw -= amount0; + assets.token1Raw -= amount1; + assets.uni0 += amount0; + assets.uni1 += amount1; + + const key = uniswapPositionKey(owner, lowerTick, upperTick); + const idx = uniswapPositions.map((x) => uniswapPositionKey(owner, x.lower ?? 0, x.upper ?? 0)).indexOf(key); + + if (idx !== -1) { + const oldPosition = { ...uniswapPositions[idx] }; + oldPosition.liquidity = JSBI.add(oldPosition.liquidity, liquidity); + uniswapPositions[idx] = oldPosition; + } else { + uniswapPositions.push({ liquidity, lower: lowerTick, upper: upperTick }); + } + + return { ...operand, assets, uniswapPositions }; +} + +export function removeLiquidityOperator( + operand: ActionCardOperand, + owner: Address, + liquidity: JSBI, + lowerTick: number, + upperTick: number, + currentTick: number, + token0Decimals: number, + token1Decimals: number +): ActionCardOperand | null { + const assets = { ...operand.assets }; + const uniswapPositions = operand.uniswapPositions.concat(); + + const [amount0, amount1] = getAmountsForLiquidity( + liquidity, + lowerTick, + upperTick, + currentTick, + token0Decimals, + token1Decimals + ); + + assets.token0Raw += amount0; + assets.token1Raw += amount1; + assets.uni0 -= amount0; + assets.uni1 -= amount1; + + const key = uniswapPositionKey(owner, lowerTick, upperTick); + const idx = uniswapPositions.map((x) => uniswapPositionKey(owner, x.lower ?? 0, x.upper ?? 0)).indexOf(key); + + if (idx === -1) { + console.error("Attempted to remove liquidity from a position that doens't exist"); + return null; + } + + const oldPosition = { ...uniswapPositions[idx] }; + if (JSBI.lessThan(oldPosition.liquidity, liquidity)) { + console.error('Attempted to remove more than 100% of liquidity from a position'); + return null; + } + oldPosition.liquidity = JSBI.subtract(oldPosition.liquidity, liquidity); + uniswapPositions[idx] = oldPosition; + + return { ...operand, assets, uniswapPositions }; +} diff --git a/prime/src/data/actions/Actions.ts b/prime/src/data/actions/Actions.ts index 0e5919c1b..bc69d9a2f 100644 --- a/prime/src/data/actions/Actions.ts +++ b/prime/src/data/actions/Actions.ts @@ -51,10 +51,19 @@ export type UniswapResult = { isAmount0LastUpdated?: boolean; }; +export interface ActionCardOperand { + readonly assets: Assets; + readonly liabilities: Liabilities; + readonly uniswapPositions: readonly UniswapPosition[]; + readonly availableBalances: UserBalances; + readonly requiredAllowances: UserBalances; +} + export type ActionCardState = { actionId: ActionID; actionArgs?: string; textFields?: string[]; + operator: (operand: ActionCardOperand | null) => ActionCardOperand | null; aloeResult: AloeResult | null; uniswapResult: UniswapResult | null; }; @@ -188,24 +197,36 @@ export const ActionTemplates: { [key: string]: ActionTemplate } = { textFields: ['100'], aloeResult: { selectedToken: TokenType.ASSET0 }, uniswapResult: null, + operator(operand) { + return null; + }, }, { actionId: MINT_TOKEN_PLUS.id, textFields: ['100'], aloeResult: { selectedToken: TokenType.ASSET0 }, uniswapResult: null, + operator(operand) { + return null; + }, }, { actionId: BORROW.id, textFields: ['0.044'], aloeResult: { selectedToken: TokenType.ASSET1 }, uniswapResult: null, + operator(operand) { + return null; + }, }, { actionId: WITHDRAW.id, textFields: ['0.044'], aloeResult: { selectedToken: TokenType.ASSET1 }, uniswapResult: null, + operator(operand) { + return null; + }, }, ], }, @@ -222,6 +243,9 @@ export const ActionTemplates: { [key: string]: ActionTemplate } = { selectedToken: TokenType.ASSET0, }, uniswapResult: null, + operator(operand) { + return null; + }, }, { actionId: BORROW.id, @@ -232,6 +256,9 @@ export const ActionTemplates: { [key: string]: ActionTemplate } = { selectedToken: TokenType.ASSET0, }, uniswapResult: null, + operator(operand) { + return null; + }, }, { actionId: BORROW.id, @@ -242,11 +269,17 @@ export const ActionTemplates: { [key: string]: ActionTemplate } = { selectedToken: TokenType.ASSET1, }, uniswapResult: null, + operator(operand) { + return null; + }, }, { actionId: ADD_LIQUIDITY.id, aloeResult: null, uniswapResult: null, + operator(operand) { + return null; + }, }, ], }, diff --git a/prime/src/pages/BorrowActionsPage.tsx b/prime/src/pages/BorrowActionsPage.tsx index 5d170b64e..aa631b384 100644 --- a/prime/src/pages/BorrowActionsPage.tsx +++ b/prime/src/pages/BorrowActionsPage.tsx @@ -379,6 +379,9 @@ export default function BorrowActionsPage() { actionId: action.id, aloeResult: null, uniswapResult: null, + operator(operand) { + return null; + }, }, ]); setActiveActions([...activeActions, action]); @@ -399,6 +402,9 @@ export default function BorrowActionsPage() { actionId: x.id, aloeResult: null, uniswapResult: null, + operator(operand) { + return null; + }, }; }); updateActionResults([...actionResults, ...newActionResults]);