diff --git a/components/TokenSelect.vue b/components/TokenSelect.vue new file mode 100644 index 0000000..3bac487 --- /dev/null +++ b/components/TokenSelect.vue @@ -0,0 +1,81 @@ + + + \ No newline at end of file diff --git a/components/TokenSelectOption.vue b/components/TokenSelectOption.vue new file mode 100644 index 0000000..98b8ae1 --- /dev/null +++ b/components/TokenSelectOption.vue @@ -0,0 +1,25 @@ + + + \ No newline at end of file diff --git a/components/common/input/InputAmount.vue b/components/common/input/InputAmount.vue index 7e2474d..75168ad 100644 --- a/components/common/input/InputAmount.vue +++ b/components/common/input/InputAmount.vue @@ -92,7 +92,7 @@ \ No newline at end of file diff --git a/components/sidebar/context/strategy/SidebarStrategySelection.vue b/components/sidebar/context/strategy/SidebarStrategySelection.vue new file mode 100644 index 0000000..e1218e7 --- /dev/null +++ b/components/sidebar/context/strategy/SidebarStrategySelection.vue @@ -0,0 +1,56 @@ + + + \ No newline at end of file diff --git a/composables/protocols/useAaveV2Position.ts b/composables/protocols/useAaveV2Position.ts index f893894..8f4efbf 100644 --- a/composables/protocols/useAaveV2Position.ts +++ b/composables/protocols/useAaveV2Position.ts @@ -12,6 +12,7 @@ import { useBigNumber } from "~/composables/useBigNumber"; import { usePosition } from "~/composables/usePosition"; import { useToken } from "~/composables/useToken"; import { useSorting } from "~/composables/useSorting"; +import useEventBus from "../useEventBus"; const { times, @@ -25,7 +26,7 @@ const { } = useBigNumber(); const { getType } = usePosition(); -const position = ref({ +export const position = ref({ totalSupplyInEth: new BigNumber(0), totalBorrowInEth: new BigNumber(0), totalBorrowStableInEth: new BigNumber(0), @@ -69,6 +70,7 @@ export function useAaveV2Position( const { activeAccount } = useDSA(); const { getTokenByKey, allATokensV2 } = useToken(); const { byMaxSupplyOrBorrowDesc } = useSorting() + const { onEvent } = useEventBus() const resolver = computed(() => chainId.value === 1 @@ -107,6 +109,7 @@ export function useAaveV2Position( position.value = await fetchPosition(); }; + onEvent("protocol::aaveV2::refresh", refreshPosition); watch( web3, diff --git a/composables/protocols/useCompoundPosition.ts b/composables/protocols/useCompoundPosition.ts index 7078957..ece0239 100644 --- a/composables/protocols/useCompoundPosition.ts +++ b/composables/protocols/useCompoundPosition.ts @@ -14,6 +14,7 @@ import addresses from "~/constant/addresses"; import ctokens from "~/constant/ctokens"; import tokenIdMapping from "~/constant/tokenIdMapping"; import { useSorting } from "~/composables/useSorting"; +import useEventBus from "../useEventBus"; const { times, @@ -27,7 +28,7 @@ const { } = useBigNumber(); const { getType } = usePosition(); -const position = ref({ +export const position = ref({ totalSupplyInEth: new BigNumber(0), totalBorrowInEth: new BigNumber(0), totalBorrowStableInEth: new BigNumber(0), @@ -62,6 +63,7 @@ export function useCompoundPosition( ) { overridePosition = overridePosition || (pos => pos); + const { onEvent } = useEventBus() const { web3, networkName } = useWeb3(); const { activeAccount } = useDSA(); const { getTokenByKey } = useToken(); @@ -100,6 +102,8 @@ export function useCompoundPosition( position.value = await fetchPosition(); }; + onEvent("protocol::compound::refresh", refreshPosition); + watch( web3, async val => { diff --git a/composables/protocols/useLiquityPosition.ts b/composables/protocols/useLiquityPosition.ts index 87d3258..1a51382 100644 --- a/composables/protocols/useLiquityPosition.ts +++ b/composables/protocols/useLiquityPosition.ts @@ -9,8 +9,9 @@ BigNumber.config({ POW_PRECISION: 200 }); import abis from "~/constant/abis"; import addresses from "~/constant/addresses"; import { useDSA } from "../useDSA"; +import useEventBus from "../useEventBus"; -const trove = ref({ +export const trove = ref({ collateral: "0", debt: "0", stabilityAmount: "0", @@ -26,7 +27,7 @@ const trove = ref({ liquidation: "0" }); -const troveTypes = ref([ +export const troveTypes = ref([ { totalCollateral: "0", price: "0", @@ -41,7 +42,7 @@ const troveTypes = ref([ } ]); -const troveOverallDetails = computed(() => +export const troveOverallDetails = computed(() => troveTypes.value.find(t => t.tokenKey === trove.value.tokenKey) ); @@ -49,6 +50,7 @@ export function useLiquityPosition( collateralAmountRef: Ref = null, debtAmountRef: Ref = null ) { + const { onEvent } = useEventBus() const { web3 } = useWeb3(); const { activeAccount } = useDSA(); @@ -178,6 +180,9 @@ export function useLiquityPosition( } } + onEvent("protocol::liquity::refresh", fetchPosition); + + watch( web3, async val => { diff --git a/composables/protocols/useMakerdaoPosition.ts b/composables/protocols/useMakerdaoPosition.ts index d6c1394..aad5397 100644 --- a/composables/protocols/useMakerdaoPosition.ts +++ b/composables/protocols/useMakerdaoPosition.ts @@ -9,6 +9,7 @@ import { useDSA } from "~/composables/useDSA"; import { useToken } from "~/composables/useToken"; import { useWeb3 } from "~/composables/useWeb3"; import { AbiItem } from "web3-utils"; +import useEventBus from "../useEventBus"; const defaultVault = { id: null, @@ -31,7 +32,7 @@ const isNewVault = ref(false); const vaultTypes = ref([]); const vaultType = ref(""); -const vault = computed(() => { +export const vault = computed(() => { const vlt = vaults.value.find(v => v.id === vaultId.value); if (!isNewVault.value && !!vlt) { return vlt; @@ -54,6 +55,7 @@ export function useMakerdaoPosition( collateralAmountRef: Ref = null, debtAmountRef: Ref = null ) { + const { onEvent } = useEventBus() const { web3, chainId, networkName } = useWeb3(); const { activeAccount } = useDSA(); const { isZero, ensureValue, times, div, max, gt } = useBigNumber(); @@ -132,6 +134,8 @@ export function useMakerdaoPosition( } }; + onEvent("protocol::makerdao::refresh", fetchPosition); + watch( web3, async val => { diff --git a/composables/useBalances.ts b/composables/useBalances.ts index 6703772..b53cd2a 100644 --- a/composables/useBalances.ts +++ b/composables/useBalances.ts @@ -20,8 +20,14 @@ import { useBigNumber } from "./useBigNumber"; import { useSorting } from "./useSorting"; const balances = reactive({ - user: null, - dsa: null + user: { + mainnet: {}, + polygon: {} + }, + dsa: { + mainnet: {}, + polygon: {} + } }); const prices = reactive({ @@ -206,6 +212,7 @@ async function getBalances( } const { name, symbol, decimals, type, isStableCoin, key } = tokenData; tokensBalObj[tokenAddress] = { + address: tokenAddress, name, symbol, decimals, diff --git a/composables/useEventBus.ts b/composables/useEventBus.ts new file mode 100644 index 0000000..af8ef00 --- /dev/null +++ b/composables/useEventBus.ts @@ -0,0 +1,24 @@ +import { onBeforeUnmount } from "@nuxtjs/composition-api"; +import { TinyEmitter } from "tiny-emitter"; + +const eventEmitter = new TinyEmitter(); + +export default function useEventBus() { + const eventHandlers = []; + + onBeforeUnmount(() => + eventHandlers.forEach(eventHandler => + eventEmitter.off(eventHandler.event, eventHandler.handler) + ) + ); + + return { + onEvent(event, handler) { + eventHandlers.push({ event, handler }); + eventEmitter.on(event, handler); + }, + emitEvent(event, payload) { + eventEmitter.emit(event, payload); + } + }; +} diff --git a/composables/useSidebar.ts b/composables/useSidebar.ts index a8ee8c2..8feb30e 100644 --- a/composables/useSidebar.ts +++ b/composables/useSidebar.ts @@ -35,11 +35,16 @@ import SidebarLiquityTroveWithdraw from '~/components/sidebar/context/liquity/Si import SidebarLiquityTroveBorrow from '~/components/sidebar/context/liquity/SidebarLiquityTroveBorrow.vue' import SidebarLiquityTrovePayback from '~/components/sidebar/context/liquity/SidebarLiquityTrovePayback.vue' +import SidebarStrategySelection from '~/components/sidebar/context/strategy/SidebarStrategySelection.vue' +import SidebarStrategy from '~/components/sidebar/context/strategy/SidebarStrategy.vue' const sidebars = { "#overview" : {component: SidebarOverview, back : false, close : true }, "#deposit-overview": {component: SidebarDepositOverview, back: { hash: 'overview' } }, '#withdraw-token': { component: SidebarWithdraw, back: { hash: 'overview' } }, + '#strategies': { component: SidebarStrategySelection }, + '#strategy': { component: SidebarStrategy }, + "/aave-v2": { component: null }, "/aave-v2#supply": { component: SidebarAaveV2Supply }, "/aave-v2#borrow": { component: SidebarAaveV2Borrow }, diff --git a/composables/useStrategy.ts b/composables/useStrategy.ts new file mode 100644 index 0000000..ff0ef4b --- /dev/null +++ b/composables/useStrategy.ts @@ -0,0 +1,157 @@ +import { + nextTick, + onMounted, + ref, + watch, + watchEffect +} from "@nuxtjs/composition-api"; +import tokens from "~/constant/tokens"; +import { + buildStrategy, + DefineStrategy, + StrategyProtocol +} from "~/core/strategies"; +import { position as aaveV2Position } from "./protocols/useAaveV2Position"; +import { position as compoundPosition } from "./protocols/useCompoundPosition"; +import { vault as makerPosition } from "./protocols/useMakerdaoPosition"; +import { + trove as liquityPosition, + troveTypes, + troveOverallDetails +} from "./protocols/useLiquityPosition"; +import { useBalances } from "./useBalances"; +import { useDSA } from "./useDSA"; +import useEventBus from "./useEventBus"; +import { useNotification } from "./useNotification"; +import { useSidebar } from "./useSidebar"; +import { useToken } from "./useToken"; +import { useWeb3 } from "./useWeb3"; +import { useBigNumber } from "./useBigNumber"; +import tokenIdMapping from "~/constant/tokenIdMapping"; +import { useFormatting } from "./useFormatting"; + +export function useStrategy(defineStrategy: DefineStrategy) { + const { web3, networkName, account } = useWeb3(); + const { dsa } = useDSA(); + const { prices, balances, fetchBalances } = useBalances(); + const { close } = useSidebar(); + const { valInt, getTokenByKey } = useToken(); + const { emitEvent } = useEventBus(); + const { toBN } = useBigNumber(); + const formatting = useFormatting(); + + const { + showPendingTransaction, + showConfirmedTransaction + } = useNotification(); + + const strategy = buildStrategy(defineStrategy); + const components = ref(strategy.components); + const error = ref(""); + const pending = ref(false); + + // strategy.onUpdated(async () => { + // await nextTick(); + // }); + + const submit = async () => { + error.value = ""; + pending.value = true; + try { + const tx = await strategy.submit({ + onReceipt: async () => { + showConfirmedTransaction(tx); + await fetchBalances(true); + + emitEvent(`protocol::${strategy.schema.protocol}::refresh`, {}); + }, + from: account.value + }); + showPendingTransaction(tx); + close(); + } catch (e) { + error.value = e.message; + } + pending.value = false; + }; + + watch( + () => [ + aaveV2Position, + makerPosition, + compoundPosition, + liquityPosition, + troveTypes, + troveOverallDetails + ], + () => { + let position = null; + let positionExtra = {}; + + if (strategy.schema.protocol == StrategyProtocol.AAVE_V2) { + position = aaveV2Position.value; + } else if (strategy.schema.protocol == StrategyProtocol.MAKERDAO) { + position = makerPosition.value; + } else if (strategy.schema.protocol == StrategyProtocol.COMPOUND) { + position = compoundPosition.value; + } else if (strategy.schema.protocol == StrategyProtocol.LIQUITY) { + position = liquityPosition.value; + + positionExtra["troveTypes"] = troveTypes.value; + positionExtra["troveOverallDetails"] = troveOverallDetails.value; + } + + strategy.setProps({ + convertTokenAmountToWei: valInt, + getTokenByKey, + toBN, + position, + positionExtra, + tokenIdMapping, + formatting + }); + }, + { immediate: true } + ); + + watch(web3, () => strategy.setWeb3(web3.value), { immediate: true }); + watch(dsa, () => strategy.setDSA(dsa.value), { immediate: true }); + watch( + prices, + () => strategy.setProps({ prices: prices[networkName.value] }), + { immediate: true } + ); + watch( + balances, + () => { + strategy.setProps({ + dsaBalances: balances.dsa[networkName.value], + userBalances: balances.user[networkName.value] + }); + }, + { immediate: true } + ); + watch( + networkName, + () => + strategy.setProps({ + tokens: tokens[networkName.value].allTokens, + tokenKeys: tokens[networkName.value].tokenKeys + }), + { immediate: true } + ); + + // testing + onMounted(() => { + //@ts-ignore + window.strategy = strategy; + }); + + return { + strategy, + components, + submit, + error, + pending + }; +} diff --git a/composables/useTenderly.ts b/composables/useTenderly.ts index 753615f..05c1d2a 100644 --- a/composables/useTenderly.ts +++ b/composables/useTenderly.ts @@ -54,14 +54,16 @@ export function useTenderly() { const stopSimulation = async () => { loading.value = true; try { - await axios({ - method: "delete", - url: `https://api.tenderly.co/api/v1/account/${$config.TENDERLY_FORK_PATH}/fork/${forkId.value}`, - headers: { - "X-Access-key": $config.TENDERLY_KEY, - "Content-Type": "application/json" - } - }); + if (forkId.value) { + await axios({ + method: "delete", + url: `https://api.tenderly.co/api/v1/account/${$config.TENDERLY_FORK_PATH}/fork/${forkId.value}`, + headers: { + "X-Access-key": $config.TENDERLY_KEY, + "Content-Type": "application/json" + } + }); + } } catch (error) {} forkId.value = null; @@ -106,6 +108,6 @@ export function useTenderly() { canSimulate, startSimulation, stopSimulation, - loading, + loading }; } diff --git a/composables/useWeb3.ts b/composables/useWeb3.ts index 9f6f1f4..75ecefc 100644 --- a/composables/useWeb3.ts +++ b/composables/useWeb3.ts @@ -1,4 +1,4 @@ -import { computed, onMounted, ref, watch } from "@nuxtjs/composition-api"; +import { computed, onMounted, ref } from "@nuxtjs/composition-api"; import Web3 from "web3"; import { SafeAppWeb3Modal } from "@gnosis.pm/safe-apps-web3modal"; import { Network } from "./useNetwork"; @@ -134,10 +134,6 @@ export function useWeb3() { web3.value = newWeb3; }; - watch(web3, () => { - window.web3 = web3.value; - }); - return { account, chainId, diff --git a/core/strategies/helpers/index.ts b/core/strategies/helpers/index.ts new file mode 100644 index 0000000..1c8bdc5 --- /dev/null +++ b/core/strategies/helpers/index.ts @@ -0,0 +1,151 @@ +import DSA, { Spell } from "dsa-connect"; +import Web3 from "web3"; +import slugify from "slugify"; +import { Strategy } from "./strategy"; +import BigNumber from "bignumber.js"; +import tokenIdMapping from "~/constant/tokenIdMapping"; +import { useFormatting } from "~/composables/useFormatting"; +export interface IStrategyContext { + dsa: DSA; + web3: Web3; + components: IStrategyComponent[]; + + // TODO: add types in useStrategy.ts + dsaBalances?: { [address: string]: IStrategyToken }; + userBalances?: { [address: string]: IStrategyToken }; + tokens?: { [address: string]: IStrategyToken }; + convertTokenAmountToWei?: (value: any, decimals: any) => string; + getTokenByKey?: (key: string) => IStrategyToken; + position?: any; + positionExtra?: { [key: string]: any }; + variables?: { [key: string]: any }; + toBN?: (value: any) => BigNumber; + tokenIdMapping?: typeof tokenIdMapping; + formatting?: ReturnType; +} + +export interface IStrategyToken { + address: string; + key: string; + symbol: string; + balance: string; + decimals: string; + + // supply: string; + // borrow: string; +} + +export enum StrategyComponentType { + // INPUT = "input", + INPUT_NUMERIC = "input-numeric", + INPUT_AMOUNT = "input-amount", + INPUT_WITH_TOKEN = "input-with-token", + + HEADING = "heading", + VALUE = "value", + STATUS = "status" +} + +export type StrategyComponentParameterMap = { + // [StrategyInputType.INPUT]: {}; + + [StrategyComponentType.INPUT_NUMERIC]: {}; + + [StrategyComponentType.INPUT_AMOUNT]: { + tokenKey: string; + }; + + [StrategyComponentType.INPUT_WITH_TOKEN]: { + token?: IStrategyToken; + }; + + [StrategyComponentType.HEADING]: {}; + [StrategyComponentType.VALUE]: {}; + [StrategyComponentType.STATUS]: { + liquidation?: any; + status?: any; + }; +}; + +export interface IStrategyComponent< + ComponentType extends StrategyComponentType +> { + type: ComponentType; + name: string; + + variables?: { [key: string]: any }; + + placeholder?: ( + context: IStrategyContext & { + component: IStrategyComponent & + StrategyComponentParameterMap[ComponentType]; + } + ) => string; + validate?: ( + context: IStrategyContext & { + component: IStrategyComponent & + StrategyComponentParameterMap[ComponentType]; + } + ) => string | void; + + defaults?: (context: Omit) => object; + update?: ( + context: IStrategyContext & { + component: IStrategyComponent & + StrategyComponentParameterMap[ComponentType]; + } + ) => void; + + value?: any; + + [key: string]: any; +} + +export enum StrategyProtocol { + AAVE_V2 = "aaveV2", + COMPOUND = "compound", + MAKERDAO = "makerdao", + LIQUITY = "liquity" +} +export interface IStrategy { + protocol: StrategyProtocol; + id?: string; + name: string; + description: string; + details?: string; + author?: string; + + components: IStrategyComponent[]; + + variables?: object; + + spells: (context: IStrategyContext) => Promise | Spell[]; + validate?: ( + context: IStrategyContext + ) => Promise | void | string; + + submitText?: string; +} + +export function defineStrategyComponent< + ComponentType extends StrategyComponentType +>( + component: IStrategyComponent & + StrategyComponentParameterMap[ComponentType] +) { + return component as IStrategyComponent & + StrategyComponentParameterMap[ComponentType]; +} + +export function defineStrategy(strategy: IStrategy) { + return { + ...strategy, + id: strategy.id ? strategy.id : slugify(strategy.name).toLowerCase() + }; +} + +export function buildStrategy(schema: DefineStrategy) { + return new Strategy(schema); +} + +export type DefineStrategy = ReturnType; diff --git a/core/strategies/helpers/strategy.ts b/core/strategies/helpers/strategy.ts new file mode 100644 index 0000000..8ba043d --- /dev/null +++ b/core/strategies/helpers/strategy.ts @@ -0,0 +1,197 @@ +import DSA from "dsa-connect"; +import Web3 from "web3"; +import { DefineStrategy, IStrategyContext } from "."; + +export class Strategy { + schema: DefineStrategy; + components = []; + context = { + web3: null as Web3, + dsa: null as DSA + }; + + listeners = []; + + props: object = { + prices: {}, + dsaTokens: {}, + userTokens: {} + }; + + constructor(schema: DefineStrategy) { + this.schema = schema; + + this.components = this.generateComponents(this.schema.components); + } + + getBaseContext(): Omit { + return { + ...this.context, + ...this.props, + variables: this.schema.variables || {} + }; + } + + getContext(): IStrategyContext { + return { + ...this.getBaseContext(), + components: this.components + }; + } + + setProps(props: object) { + Object.assign(this.props, props); + + const components = this.components; + + for (const component of components) { + if (typeof component.defaults !== "function") { + continue; + } + + if (component.defaulted) { + continue; + } + + Object.assign(component, component.defaults(this.getBaseContext())); + + component.defaulted = true; + } + + this.notifyListeners("SET_PROPS"); + } + + generateComponents(components) { + return components.map((component, idx) => { + const computedComponent = { + ...component, + value: component.value || "", + error: component.error || "", + placeholder: () => { + return component.placeholder + ? component.placeholder({ + ...this.getContext(), + component: this.components[idx] + }) + : null; + }, + onInput: (val: any) => { + this.components[idx].error = ""; + this.components[idx].value = val; + + if (val) { + this.components[idx].error = this.components[idx].validate({ + ...this.getContext(), + component: this.components[idx] + }); + } + + this.notifyListeners("onInput"); + }, + onCustomInput: (values: object) => { + this.components[idx] = Object.assign(this.components[idx], values); + + this.components[idx].error = this.components[idx].validate({ + ...this.getContext(), + component: this.components[idx] + }); + this.notifyListeners("onCustomInput"); + } + }; + + let defaults = {}; + + if (component.defaults) { + defaults = component.defaults(this.getBaseContext()); + } + + return { + ...computedComponent, + ...defaults + }; + }); + } + + async spells() { + return await this.schema.spells(this.getContext()); + } + + async submit(options) { + await this.validate(); + + const allSpells = await this.spells(); + + const spells = this.context.dsa.Spell(); + + for (const spell of allSpells) { + spells.add(spell); + } + + return await this.context.dsa.cast({ + spells, + onReceipt: options?.onReceipt, + from: options?.from + }); + } + + async validate() { + const components = this.components; + + for (const component of components) { + if (typeof component.validate !== "function") { + continue; + } + + const result = await component.validate({ + ...this.getContext(), + component + }); + + if (typeof result === "string") { + throw new Error(result || "Error has occurred"); + } + } + + if (this.schema.validate) { + const result = await this.schema.validate(this.getContext()); + + if (typeof result === "string") { + throw new Error(result || "Error has occurred"); + } + } + } + + setWeb3(web3: Web3) { + this.context.web3 = web3; + + this.notifyListeners("WEB3"); + } + + setDSA(dsa: DSA) { + this.context.dsa = dsa; + + this.notifyListeners("DSA"); + } + + async notifyListeners( from = "") { + + if(from && process.env.NODE_ENV === "development") { + console.log(`${from} updated`); + } + + for (const listener of this.listeners) { + await listener(this); + } + + this.components.forEach(component => + component.update?.({ + ...this.getContext(), + component + }) + ); + } + + onUpdated(cb) { + this.listeners.push(cb); + } +} diff --git a/core/strategies/index.ts b/core/strategies/index.ts new file mode 100644 index 0000000..793bcd8 --- /dev/null +++ b/core/strategies/index.ts @@ -0,0 +1,11 @@ +import AaveV2 from "./protocols/aave-v2"; +import Compound from "./protocols/compound"; +import Liquity from "./protocols/liquity"; + +export const protocolStrategies = { + aaveV2: AaveV2, + compound: Compound, + liquity: Liquity +}; + +export * from "./helpers"; diff --git a/core/strategies/protocols/aave-v2/deposit-and-borrow.ts b/core/strategies/protocols/aave-v2/deposit-and-borrow.ts new file mode 100644 index 0000000..c53db4d --- /dev/null +++ b/core/strategies/protocols/aave-v2/deposit-and-borrow.ts @@ -0,0 +1,266 @@ +import BigNumber from "bignumber.js"; +import { + defineStrategy, + defineStrategyComponent, + StrategyComponentType, + StrategyProtocol +} from "../../helpers"; + +export default defineStrategy({ + protocol: StrategyProtocol.AAVE_V2, + name: "Deposit & Borrow", + description: "Deposit collateral & borrow asset in a single txn.", + + details: `

This strategy executes:

+
    +
  • Deposit collateral
  • +
  • Borrow Debt
  • +
`, + + submitText: "Deposit & Borrow", + + author: "Instadapp Team", + + variables: { + collateralTokenKey: "eth", + debtTokenKey: "dai", + debtRateMode: 2 + }, + + components: [ + defineStrategyComponent({ + type: StrategyComponentType.INPUT_WITH_TOKEN, + name: "Collateral", + placeholder: ({ component: input }) => + input.token ? `${input.token.symbol} to Deposit` : "", + validate: ({ component: input, dsaBalances, toBN }) => { + if (!input.token) { + return "Collateral token is required"; + } + + if (!input.value) { + return "Collateral amount is required"; + } + + const collateralBalance = toBN( + dsaBalances[input.token.address]?.balance + ); + + if (toBN(collateralBalance).lt(input.value)) { + const collateralBalanceFormatted = collateralBalance.toFixed(2); + return `Your amount exceeds your maximum limit of ${collateralBalanceFormatted} ${input.token.symbol}`; + } + }, + defaults: ({ getTokenByKey, variables }) => ({ + token: getTokenByKey?.(variables.collateralTokenKey) + }) + }), + + defineStrategyComponent({ + type: StrategyComponentType.INPUT_WITH_TOKEN, + name: "Debt", + placeholder: ({ component: input }) => + input.token ? `${input.token.symbol} to Borrow` : "", + validate: ({ component: input }) => { + if (!input.token) { + return "Debt token is required"; + } + + if (!input.value) { + return "Debt amount is required"; + } + }, + defaults: ({ getTokenByKey, variables }) => ({ + token: getTokenByKey?.(variables.debtTokenKey) + }) + }), + defineStrategyComponent({ + type: StrategyComponentType.HEADING, + name: "Projected Debt Position" + }), + defineStrategyComponent({ + type: StrategyComponentType.STATUS, + name: "Status", + update: ({ position, component, components, toBN }) => { + if ( + toBN(components[0].value).isZero() && + toBN(components[1].value).isZero() + ) { + return; + } + + if (!position) { + return; + } + + const newPositionData = changedPositionData(position, components); + const stats = calculateStats(newPositionData); + + component.liquidation = BigNumber.max( + toBN(stats.totalMaxBorrowLimitInEth).div(stats.totalSupplyInEth), + "0" + ).toFixed(); + component.status = BigNumber.max( + toBN(stats.totalBorrowInEth).div(stats.totalSupplyInEth), + "0" + ).toFixed(); + } + }), + defineStrategyComponent({ + type: StrategyComponentType.VALUE, + name: "LIQUIDATION PRICE (IN ETH)", + value: "-", + update: ({ position, component, components, toBN, formatting }) => { + if (!position) { + return; + } + + const newPositionData = changedPositionData(position, components); + const initialStats = calculateStats(position.data); + const newStats = calculateStats(newPositionData); + + const stats = + toBN(components[0].value).isZero() && + toBN(components[1].value).isZero() + ? initialStats + : newStats; + + let liquidationPrice = "0"; + if (!toBN(stats.ethSupplied).isZero()) { + liquidationPrice = BigNumber.max( + toBN(stats.totalBorrowInEth) + .div(stats.totalMaxLiquidationLimitInEth) + .times(position.ethPriceInUsd), + "0" + ).toFixed(); + } + + component.value = `${formatting.formatUsdMax( + liquidationPrice, + position.ethPriceInUsd + )} / ${formatting.formatUsd(position.ethPriceInUsd)}`; + } + }) + ], + + validate: async ({ position, components: inputs, toBN }) => { + if (toBN(inputs[0].value).isZero() && toBN(inputs[1].value).isZero()) { + return; + } + + const newPositionData = changedPositionData(position, inputs); + const stats = calculateStats(newPositionData); + + let liquidation = "0"; + if (!toBN(stats.totalSupplyInEth).isZero()) { + liquidation = BigNumber.max( + toBN(stats.totalMaxBorrowLimitInEth).div(stats.totalSupplyInEth), + "0" + ).toFixed(); + } + + const status = BigNumber.max( + toBN(stats.totalBorrowInEth).div(stats.totalSupplyInEth), + "0" + ); + + if (status.gt(toBN(liquidation).minus("0.0001"))) { + return "Position will liquidate."; + } + }, + + spells: async ({ + components: inputs, + convertTokenAmountToWei, + variables + }) => { + return [ + { + connector: "aave_v2", + method: "deposit", + args: [ + inputs[0].token.address, + convertTokenAmountToWei(inputs[0].value, inputs[0].token.decimals), + 0, + 0 + ] + }, + { + connector: "aave_v2", + method: "borrow", + args: [ + inputs[1].token.address, + convertTokenAmountToWei(inputs[1].value, inputs[1].token.decimals), + variables.debtRateMode, + 0, + 0 + ] + } + ]; + } +}); + +const changedPositionData = (position, inputs) => { + return position.data.map(position => { + const changedPosition = { ...position }; + if (inputs[1].token.key === position.key) { + changedPosition.borrow = BigNumber.max( + new BigNumber(position.borrow).plus(inputs[1].value || "0"), + "0" + ).toFixed(); + } + + if (inputs[0].token.key === position.key) { + changedPosition.supply = BigNumber.max( + new BigNumber(position.supply).plus(inputs[0].value || "0"), + "0" + ).toFixed(); + } + + return changedPosition; + }); +}; + +const calculateStats = positionData => { + return positionData.reduce( + ( + stats, + { key, supply, borrow, borrowStable, priceInEth, factor, liquidation } + ) => { + if (key === "eth") { + stats.ethSupplied = supply; + } + + const borrowTotal = new BigNumber(borrow).plus(borrowStable); + + stats.totalSupplyInEth = new BigNumber(supply) + .times(priceInEth) + .plus(stats.totalSupplyInEth) + .toFixed(); + stats.totalBorrowInEth = new BigNumber(borrowTotal) + .times(priceInEth) + .plus(stats.totalBorrowInEth) + .toFixed(); + + stats.totalMaxBorrowLimitInEth = new BigNumber(priceInEth) + .times(factor) + .times(supply) + .plus(stats.totalMaxBorrowLimitInEth) + .toFixed(); + + stats.totalMaxLiquidationLimitInEth = new BigNumber(priceInEth) + .times(liquidation) + .times(supply) + .plus(stats.totalMaxLiquidationLimitInEth) + .toFixed(); + + return stats; + }, + { + totalSupplyInEth: "0", + totalBorrowInEth: "0", + totalMaxBorrowLimitInEth: "0", + totalMaxLiquidationLimitInEth: "0" + } + ); +}; diff --git a/core/strategies/protocols/aave-v2/index.ts b/core/strategies/protocols/aave-v2/index.ts new file mode 100644 index 0000000..7a63aaa --- /dev/null +++ b/core/strategies/protocols/aave-v2/index.ts @@ -0,0 +1,8 @@ +import depositAndBorrow from "./deposit-and-borrow" +import paybackAndWithdraw from "./payback-and-withdraw" + +export default [ + depositAndBorrow, + paybackAndWithdraw, +] + \ No newline at end of file diff --git a/core/strategies/protocols/aave-v2/payback-and-withdraw.ts b/core/strategies/protocols/aave-v2/payback-and-withdraw.ts new file mode 100644 index 0000000..fff0bfd --- /dev/null +++ b/core/strategies/protocols/aave-v2/payback-and-withdraw.ts @@ -0,0 +1,262 @@ +import BigNumber from "bignumber.js"; +import tokens from "~/constant/tokens"; +import { + defineStrategy, + defineStrategyComponent, + StrategyComponentType, + StrategyProtocol +} from "../../helpers"; + +export default defineStrategy({ + protocol: StrategyProtocol.AAVE_V2, + name: "Payback & Withdraw", + description: "Payback debt & withdraw collateral in a single txn.", + author: "Instadapp Team", + + submitText: "Payback & Withdraw", + + details: `

This strategy executes:

+
    +
  • Payback debt
  • +
  • Withdraw collateral
  • +
`, + + components: [ + defineStrategyComponent({ + type: StrategyComponentType.INPUT_WITH_TOKEN, + name: "Debt", + placeholder: ({ component: input }) => + input.token ? `${input.token.symbol} to Payback` : "", + validate: ({ component: input, toBN, dsaBalances }) => { + if (!input.token) { + return "Debt token is required"; + } + + const balance = toBN(dsaBalances[input.token.address]?.balance); + + if (toBN(balance).lt(input.value)) { + return "You don't have enough balance to payback."; + } + }, + defaults: ({ getTokenByKey }) => ({ + token: getTokenByKey?.("dai") + }) + }), + defineStrategyComponent({ + type: StrategyComponentType.INPUT_WITH_TOKEN, + name: "Collateral", + placeholder: ({ component: input }) => + input.token ? `${input.token.symbol} to Withdraw` : "", + validate: ({ component: input, position, toBN }) => { + if (!input.token) { + return "Collateral token is required"; + } + + if (!input.value) { + return "Collateral amount is required"; + } + + if (position) { + const collateralPosition = position.data.find( + item => item.key === input.token.key + ); + if (collateralPosition) { + const collateralBalance = toBN(collateralPosition.supply); + if (collateralBalance.lt(input.value)) { + const collateralBalanceFormatted = collateralBalance.toFixed(2); + return `Your amount exceeds your maximum limit of ${collateralBalanceFormatted} ${input.token.symbol}`; + } + } + } + }, + defaults: ({ getTokenByKey }) => ({ + token: getTokenByKey?.("eth") + }) + }), + defineStrategyComponent({ + type: StrategyComponentType.HEADING, + name: "Projected Debt Position" + }), + defineStrategyComponent({ + type: StrategyComponentType.STATUS, + name: "Status", + update: ({ position, component, components, toBN }) => { + if ( + toBN(components[0].value).isZero() && + toBN(components[1].value).isZero() + ) { + return; + } + + if (!position) { + return; + } + + const newPositionData = changedPositionData(position, components); + const stats = calculateStats(newPositionData); + + component.liquidation = BigNumber.max( + toBN(stats.totalMaxLiquidationLimitInEth).div(stats.totalSupplyInEth), + "0" + ).toFixed(); + component.status = BigNumber.max( + toBN(stats.totalBorrowInEth).div(stats.totalSupplyInEth), + "0" + ).toFixed(); + } + }), + defineStrategyComponent({ + type: StrategyComponentType.VALUE, + name: "LIQUIDATION PRICE (IN ETH)", + value: "-", + update: ({ position, component, components, toBN, formatting }) => { + if (!position) { + return; + } + + const newPositionData = changedPositionData(position, components); + const initialStats = calculateStats(position.data); + const newStats = calculateStats(newPositionData); + + const stats = + toBN(components[0].value).isZero() && + toBN(components[1].value).isZero() + ? initialStats + : newStats; + + let liquidationPrice = "0"; + if (!toBN(stats.ethSupplied).isZero()) { + liquidationPrice = BigNumber.max( + toBN(stats.totalBorrowInEth) + .div(stats.totalMaxLiquidationLimitInEth) + .times(position.ethPriceInUsd), + "0" + ).toFixed(); + } + + component.value = `${formatting.formatUsdMax( + liquidationPrice, + position.ethPriceInUsd + )} / ${formatting.formatUsd(position.ethPriceInUsd)}`; + } + }) + ], + + validate: async ({ position, components: inputs, toBN }) => { + if (toBN(inputs[0].value).isZero() && toBN(inputs[1].value).isZero()) { + return; + } + + const newPositionData = changedPositionData(position, inputs); + const stats = calculateStats(newPositionData); + + let maxLiquidation = "0"; + + if (!toBN(stats.totalSupplyInEth).isZero()) { + maxLiquidation = BigNumber.max( + toBN(stats.totalMaxLiquidationLimitInEth).div(stats.totalSupplyInEth), + "0" + ).toFixed(); + } + + const status = BigNumber.max( + toBN(stats.totalBorrowInEth).div(stats.totalSupplyInEth), + "0" + ); + + if (status.gt(toBN(maxLiquidation).minus("0.0001"))) { + return "Position will liquidate."; + } + }, + + spells: async ({ components: inputs, convertTokenAmountToWei }) => { + return [ + { + connector: "aave_v2", + method: "payback", + args: [ + inputs[0].token.address, + convertTokenAmountToWei(inputs[0].value, inputs[0].token.decimals), + 2, + 0, + 0 + ] + }, + { + connector: "aave_v2", + method: "withdraw", + args: [ + inputs[1].token.address, + convertTokenAmountToWei(inputs[1].value, inputs[1].token.decimals), + 0, + 0 + ] + } + ]; + } +}); + +const changedPositionData = (position, inputs) => { + return position.data.map(position => { + const changedPosition = { ...position }; + if (inputs[0].token.key === position.key) { + changedPosition.borrow = BigNumber.max( + new BigNumber(position.borrow).minus(inputs[0].value), + "0" + ).toFixed(); + } + + if (inputs[1].token.key === position.key) { + changedPosition.supply = BigNumber.max( + new BigNumber(position.supply).minus(inputs[1].value), + "0" + ).toFixed(); + } + + return changedPosition; + }); +}; + +const calculateStats = positionData => { + return positionData.reduce( + ( + stats, + { key, supply, borrow, borrowStable, priceInEth, factor, liquidation } + ) => { + if (key === "eth") { + stats.ethSupplied = supply; + } + + const borrowTotal = new BigNumber(borrow).plus(borrowStable); + + stats.totalSupplyInEth = new BigNumber(supply) + .times(priceInEth) + .plus(stats.totalSupplyInEth) + .toFixed(); + stats.totalBorrowInEth = new BigNumber(borrowTotal) + .times(priceInEth) + .plus(stats.totalBorrowInEth) + .toFixed(); + + stats.totalMaxBorrowLimitInEth = new BigNumber(priceInEth) + .times(factor) + .times(supply) + .plus(stats.totalMaxBorrowLimitInEth) + .toFixed(); + + stats.totalMaxLiquidationLimitInEth = new BigNumber(priceInEth) + .times(liquidation) + .times(supply) + .plus(stats.totalMaxLiquidationLimitInEth) + .toFixed(); + + return stats; + }, + { + totalSupplyInEth: "0", + totalBorrowInEth: "0", + totalMaxBorrowLimitInEth: "0", + totalMaxLiquidationLimitInEth: "0" + } + ); +}; diff --git a/core/strategies/protocols/compound/deposit-and-borrow.ts b/core/strategies/protocols/compound/deposit-and-borrow.ts new file mode 100644 index 0000000..12d9a89 --- /dev/null +++ b/core/strategies/protocols/compound/deposit-and-borrow.ts @@ -0,0 +1,317 @@ +import BigNumber from "bignumber.js"; +import { + defineStrategy, + defineStrategyComponent, + StrategyComponentType, + StrategyProtocol +} from "../../helpers"; + +export default defineStrategy({ + protocol: StrategyProtocol.COMPOUND, + name: "Deposit & Borrow", + description: "Deposit collateral & borrow asset in a single txn.", + + details: `

This strategy executes:

+
    +
  • Deposit collateral
  • +
  • Borrow Debt
  • +
`, + + submitText: "Deposit & Borrow", + + author: "Instadapp Team", + + variables: { + collateralTokenKey: "eth", + debtTokenKey: "dai" + }, + + components: [ + defineStrategyComponent({ + type: StrategyComponentType.INPUT_WITH_TOKEN, + name: "Collateral", + placeholder: ({ component: input }) => + input.token ? `${input.token.symbol} to Deposit` : "", + validate: ({ component: input, dsaBalances, toBN }) => { + if (!input.token) { + return "Collateral token is required"; + } + + if (!input.value) { + return "Collateral amount is required"; + } + + const collateralBalance = toBN( + dsaBalances[input.token.address]?.balance + ); + + if (toBN(collateralBalance).lt(input.value)) { + const collateralBalanceFormatted = collateralBalance.toFixed(2); + return `Your amount exceeds your maximum limit of ${collateralBalanceFormatted} ${input.token.symbol}`; + } + }, + defaults: ({ getTokenByKey, variables }) => ({ + token: getTokenByKey?.(variables.collateralTokenKey) + }) + }), + + defineStrategyComponent({ + type: StrategyComponentType.INPUT_WITH_TOKEN, + name: "Debt", + placeholder: ({ component: input }) => + input.token ? `${input.token.symbol} to Borrow` : "", + validate: ({ component: input }) => { + if (!input.token) { + return "Debt token is required"; + } + + if (!input.value) { + return "Debt amount is required"; + } + }, + defaults: ({ getTokenByKey, variables }) => ({ + token: getTokenByKey?.(variables.debtTokenKey) + }) + }), + defineStrategyComponent({ + type: StrategyComponentType.HEADING, + name: "Projected Debt Position" + }), + defineStrategyComponent({ + type: StrategyComponentType.STATUS, + name: "Status", + update: ({ position, component, components, toBN, tokenIdMapping }) => { + if ( + toBN(components[0].value).isZero() && + toBN(components[1].value).isZero() + ) { + return; + } + + if (!position) { + return; + } + + const newPositionData = changedPositionData(position, components, tokenIdMapping.tokenToId); + const stats = calculateStats(newPositionData); + + component.liquidation = BigNumber.max( + toBN(stats.totalMaxBorrowLimitInEth).div(stats.totalSupplyInEth), + "0" + ).toFixed(); + component.status = BigNumber.max( + toBN(stats.totalBorrowInEth).div(stats.totalSupplyInEth), + "0" + ).toFixed(); + } + }), + defineStrategyComponent({ + type: StrategyComponentType.VALUE, + name: "LIQUIDATION PRICE (IN ETH)", + value: "-", + update: ({ + position, + component, + components, + toBN, + formatting, + tokenIdMapping + }) => { + if (!position) { + return; + } + + const newPositionData = changedPositionData( + position, + components, + tokenIdMapping.tokenToId + ); + const initialStats = calculateStats(position.data); + const newStats = calculateStats(newPositionData); + + const stats = + toBN(components[0].value).isZero() && + toBN(components[1].value).isZero() + ? initialStats + : newStats; + + let liquidationPrice = "0"; + if (!toBN(stats.ethSupplied).isZero()) { + liquidationPrice = BigNumber.max( + toBN(stats.totalBorrowInEth) + .div(stats.totalMaxBorrowLimitInEth) + .times(position.ethPriceInUsd), + "0" + ).toFixed(); + } + + component.value = `${formatting.formatUsdMax( + liquidationPrice, + position.ethPriceInUsd + )} / ${formatting.formatUsd(position.ethPriceInUsd)}`; + } + }) + ], + + validate: async ({ position, components: inputs, toBN, tokenIdMapping }) => { + if (toBN(inputs[0].value).isZero() && toBN(inputs[1].value).isZero()) { + return; + } + const { tokenToId } = tokenIdMapping; + + const newPositionData = position.data.map(position => { + const changedPosition = { ...position }; + if (tokenToId.compound[inputs[1].token.key] === position.cTokenId) { + changedPosition.borrow = BigNumber.max( + toBN(position.borrow).plus(inputs[1].value), + "0" + ).toFixed(); + } + + if (tokenToId.compound[inputs[0].token.key] === position.cTokenId) { + changedPosition.supply = BigNumber.max( + toBN(position.supply).plus(inputs[0].value), + "0" + ).toFixed(); + } + + return changedPosition; + }); + + const stats = newPositionData.reduce( + (stats, { key, supply, borrow, priceInEth, factor }) => { + if (key === "eth") { + stats.ethSupplied = supply; + } + + stats.totalSupplyInEth = toBN(supply) + .times(priceInEth) + .plus(stats.totalSupplyInEth) + .toFixed(); + stats.totalBorrowInEth = toBN(borrow) + .times(priceInEth) + .plus(stats.totalBorrowInEth) + .toFixed(); + + stats.totalMaxBorrowLimitInEth = toBN(priceInEth) + .times(factor) + .times(supply) + .plus(stats.totalMaxBorrowLimitInEth) + .toFixed(); + + return stats; + }, + { + totalSupplyInEth: "0", + totalBorrowInEth: "0", + totalMaxBorrowLimitInEth: "0", + ethSupplied: "0" + } + ); + + let liquidation = "0"; + if (!toBN(stats.totalSupplyInEth).isZero()) { + liquidation = BigNumber.max( + toBN(stats.totalMaxBorrowLimitInEth).div(stats.totalSupplyInEth), + "0" + ).toFixed(); + } + + const status = BigNumber.max( + toBN(stats.totalBorrowInEth).div(stats.totalSupplyInEth), + "0" + ); + + if (status.gt(toBN(liquidation).minus("0.0001"))) { + return "Position will liquidate."; + } + }, + + spells: async ({ + components: inputs, + convertTokenAmountToWei, + tokenIdMapping + }) => { + const { tokenToId } = tokenIdMapping; + + const collateralTokenId = tokenToId.compound[inputs[0].token.key]; + const debtTokenId = tokenToId.compound[inputs[1].token.key]; + + return [ + { + connector: "compound", + method: "deposit", + args: [ + collateralTokenId, + convertTokenAmountToWei(inputs[0].value, inputs[0].token.decimals), + 0, + 0 + ] + }, + { + connector: "compound", + method: "borrow", + args: [ + debtTokenId, + convertTokenAmountToWei(inputs[1].value, inputs[1].token.decimals), + 0, + 0 + ] + } + ]; + } +}); + +const changedPositionData = (position, inputs, tokenToId) => { + return position.data.map(position => { + const changedPosition = { ...position }; + if (tokenToId.compound[inputs[1].token.key] === position.cTokenId) { + changedPosition.borrow = BigNumber.max( + new BigNumber(position.borrow).plus(inputs[1].value), + "0" + ).toFixed(); + } + + if (tokenToId.compound[inputs[0].token.key] === position.cTokenId) { + changedPosition.supply = BigNumber.max( + new BigNumber(position.supply).plus(inputs[0].value), + "0" + ).toFixed(); + } + + return changedPosition; + }); +}; + +const calculateStats = positionData => { + return positionData.reduce( + (stats, { key, supply, borrow, priceInEth, factor }) => { + if (key === "eth") { + stats.ethSupplied = supply; + } + + stats.totalSupplyInEth = new BigNumber(supply) + .times(priceInEth) + .plus(stats.totalSupplyInEth) + .toFixed(); + stats.totalBorrowInEth = new BigNumber(borrow) + .times(priceInEth) + .plus(stats.totalBorrowInEth) + .toFixed(); + + stats.totalMaxBorrowLimitInEth = new BigNumber(priceInEth) + .times(factor) + .times(supply) + .plus(stats.totalMaxBorrowLimitInEth) + .toFixed(); + + return stats; + }, + { + totalSupplyInEth: "0", + totalBorrowInEth: "0", + totalMaxBorrowLimitInEth: "0", + ethSupplied: "0" + } + ); +}; diff --git a/core/strategies/protocols/compound/index.ts b/core/strategies/protocols/compound/index.ts new file mode 100644 index 0000000..7a63aaa --- /dev/null +++ b/core/strategies/protocols/compound/index.ts @@ -0,0 +1,8 @@ +import depositAndBorrow from "./deposit-and-borrow" +import paybackAndWithdraw from "./payback-and-withdraw" + +export default [ + depositAndBorrow, + paybackAndWithdraw, +] + \ No newline at end of file diff --git a/core/strategies/protocols/compound/payback-and-withdraw.ts b/core/strategies/protocols/compound/payback-and-withdraw.ts new file mode 100644 index 0000000..13a4d64 --- /dev/null +++ b/core/strategies/protocols/compound/payback-and-withdraw.ts @@ -0,0 +1,307 @@ +import BigNumber from "bignumber.js"; +import { + defineStrategy, + defineStrategyComponent, + StrategyComponentType, + StrategyProtocol +} from "../../helpers"; + +export default defineStrategy({ + protocol: StrategyProtocol.COMPOUND, + name: "Payback & Withdraw", + description: "Payback debt & withdraw collateral in a single txn.", + author: "Instadapp Team", + + submitText: "Payback & Withdraw", + + details: `

This strategy executes:

+
    +
  • Payback debt
  • +
  • Withdraw collateral
  • +
`, + + components: [ + defineStrategyComponent({ + type: StrategyComponentType.INPUT_WITH_TOKEN, + name: "Debt", + placeholder: ({ component: input }) => + input.token ? `${input.token.symbol} to Payback` : "", + validate: ({ component: input, toBN, dsaBalances }) => { + if (!input.token) { + return "Debt token is required"; + } + + const balance = toBN(dsaBalances[input.token.address]?.balance); + + if (toBN(balance).lt(input.value)) { + return "You don't have enough balance to payback."; + } + }, + defaults: ({ getTokenByKey }) => ({ + token: getTokenByKey?.("dai") + }) + }), + defineStrategyComponent({ + type: StrategyComponentType.INPUT_WITH_TOKEN, + name: "Collateral", + placeholder: ({ component: input }) => + input.token ? `${input.token.symbol} to Withdraw` : "", + validate: ({ component: input, position, toBN, tokenIdMapping }) => { + if (!input.token) { + return "Collateral token is required"; + } + + if (!input.value) { + return "Collateral amount is required"; + } + + const { tokenToId } = tokenIdMapping; + + if (position) { + const collateralBalance = toBN( + position.data.find( + pos => pos.cTokenId === tokenToId.compound[input.token.key] + )?.supply || "0" + ); + + if (collateralBalance.lt(input.value)) { + const collateralBalanceFormatted = collateralBalance.toFixed( + 2, + BigNumber.ROUND_FLOOR + ); + return `Your amount exceeds your maximum limit of ${collateralBalanceFormatted} ${input.token.symbol}`; + } + } + }, + defaults: ({ getTokenByKey }) => ({ + token: getTokenByKey?.("eth") + }) + }), + defineStrategyComponent({ + type: StrategyComponentType.HEADING, + name: "Projected Debt Position" + }), + defineStrategyComponent({ + type: StrategyComponentType.STATUS, + name: "Status", + update: ({ position, component, components, toBN, tokenIdMapping }) => { + if ( + toBN(components[0].value).isZero() && + toBN(components[1].value).isZero() + ) { + return; + } + + if (!position) { + return; + } + + const newPositionData = changedPositionData(position, components, tokenIdMapping.tokenToId); + const stats = calculateStats(newPositionData); + + component.liquidation = BigNumber.max( + toBN(stats.totalMaxLiquidationLimitInEth).div(stats.totalSupplyInEth), + "0" + ).toFixed(); + component.status = BigNumber.max( + toBN(stats.totalBorrowInEth).div(stats.totalSupplyInEth), + "0" + ).toFixed(); + } + }), + defineStrategyComponent({ + type: StrategyComponentType.VALUE, + name: "LIQUIDATION PRICE (IN ETH)", + value: "-", + update: ({ position, component, components, toBN, formatting, tokenIdMapping }) => { + if (!position) { + return; + } + + const newPositionData = changedPositionData(position, components, tokenIdMapping.tokenToId); + const initialStats = calculateStats(position.data); + const newStats = calculateStats(newPositionData); + + const stats = + toBN(components[0].value).isZero() && + toBN(components[1].value).isZero() + ? initialStats + : newStats; + + let liquidationPrice = "0"; + if (!toBN(stats.ethSupplied).isZero()) { + liquidationPrice = BigNumber.max( + toBN(stats.totalBorrowInEth) + .div(stats.totalMaxBorrowLimitInEth) + .times(position.ethPriceInUsd), + "0" + ).toFixed(); + } + + component.value = `${formatting.formatUsdMax( + liquidationPrice, + position.ethPriceInUsd + )} / ${formatting.formatUsd(position.ethPriceInUsd)}`; + } + }) + ], + + validate: async ({ position, components: inputs, toBN, tokenIdMapping }) => { + if (toBN(inputs[0].value).isZero() && toBN(inputs[1].value).isZero()) { + return; + } + const { tokenToId } = tokenIdMapping; + + const newPositionData = position.data.map(position => { + const changedPosition = { ...position }; + if (tokenToId.compound[inputs[0].token.key] === position.cTokenId) { + changedPosition.borrow = BigNumber.max( + toBN(position.borrow).minus(inputs[0].value), + "0" + ).toFixed(); + } + + if (tokenToId.compound[inputs[1].token.key] === position.cTokenId) { + changedPosition.supply = BigNumber.max( + toBN(position.supply).minus(inputs[1].value), + "0" + ).toFixed(); + } + + return changedPosition; + }); + + const stats = newPositionData.reduce( + (stats, { key, supply, borrow, priceInEth, factor }) => { + if (key === "eth") { + stats.ethSupplied = supply; + } + + stats.totalSupplyInEth = toBN(supply) + .times(priceInEth) + .plus(stats.totalSupplyInEth) + .toFixed(); + stats.totalBorrowInEth = toBN(borrow) + .times(priceInEth) + .plus(stats.totalBorrowInEth) + .toFixed(); + + stats.totalMaxBorrowLimitInEth = toBN(priceInEth) + .times(factor) + .times(supply) + .plus(stats.totalMaxBorrowLimitInEth) + .toFixed(); + + return stats; + }, + { + totalSupplyInEth: "0", + totalBorrowInEth: "0", + totalMaxBorrowLimitInEth: "0", + ethSupplied: '0', + } + ); + + let liquidation = "0"; + + if (!toBN(stats.totalSupplyInEth).isZero()) { + liquidation = BigNumber.max( + toBN(stats.totalMaxBorrowLimitInEth).div(stats.totalSupplyInEth), + "0" + ).toFixed(); + } + + const status = BigNumber.max( + toBN(stats.totalBorrowInEth).div(stats.totalSupplyInEth), + "0" + ); + + if (status.gt(toBN(liquidation).minus("0.0001"))) { + return "Position will liquidate."; + } + }, + + spells: async ({ components: inputs, convertTokenAmountToWei, tokenIdMapping }) => { + const { tokenToId } = tokenIdMapping; + + const debtTokenId = tokenToId.compound[inputs[0].token.key]; + const collateralTokenId = tokenToId.compound[inputs[1].token.key]; + + return [ + { + connector: "compound", + method: "payback", + args: [ + debtTokenId, + convertTokenAmountToWei(inputs[0].value, inputs[0].token.decimals), + 0, + 0 + ] + }, + { + connector: "compound", + method: "withdraw", + args: [ + collateralTokenId, + convertTokenAmountToWei(inputs[1].value, inputs[1].token.decimals), + 0, + 0 + ] + } + ]; + } +}); + +const changedPositionData = (position, inputs, tokenToId) => { + return position.data.map(position => { + const changedPosition = { ...position }; + if (tokenToId.compound[inputs[0].token.key] === position.cTokenId) { + changedPosition.borrow = BigNumber.max( + new BigNumber(position.borrow).minus(inputs[0].value), + "0" + ).toFixed(); + } + + if (tokenToId.compound[inputs[1].token.key] === position.cTokenId) { + changedPosition.supply = BigNumber.max( + new BigNumber(position.supply).minus(inputs[1].value), + "0" + ).toFixed(); + } + + return changedPosition; + }); +}; + +const calculateStats = positionData => { + return positionData.reduce( + (stats, { key, supply, borrow, priceInEth, factor }) => { + if (key === "eth") { + stats.ethSupplied = supply; + } + + stats.totalSupplyInEth = new BigNumber(supply) + .times(priceInEth) + .plus(stats.totalSupplyInEth) + .toFixed(); + stats.totalBorrowInEth = new BigNumber(borrow) + .times(priceInEth) + .plus(stats.totalBorrowInEth) + .toFixed(); + + stats.totalMaxBorrowLimitInEth = new BigNumber(priceInEth) + .times(factor) + .times(supply) + .plus(stats.totalMaxBorrowLimitInEth) + .toFixed(); + + return stats; + }, + { + totalSupplyInEth: "0", + totalBorrowInEth: "0", + totalMaxBorrowLimitInEth: "0", + ethSupplied: "0" + } + ); +}; diff --git a/core/strategies/protocols/liquity/close-trove.ts b/core/strategies/protocols/liquity/close-trove.ts new file mode 100644 index 0000000..616d15b --- /dev/null +++ b/core/strategies/protocols/liquity/close-trove.ts @@ -0,0 +1,104 @@ +import { + defineStrategy, + StrategyProtocol, + StrategyComponentType, + defineStrategyComponent +} from "../../helpers"; + +export default defineStrategy({ + protocol: StrategyProtocol.LIQUITY, + name: "Close Trove", + description: + "Close trove: Payback debt, withdraw Collateral & Liquidation Reserve.", + author: "Instadapp Team", + + submitText: "Close Trove", + + details: `

This strategy executes:

+
    +
  • Close Trove
  • +
`, + + components: [ + defineStrategyComponent({ + type: StrategyComponentType.HEADING, + name: "Payback" + }), + defineStrategyComponent({ + type: StrategyComponentType.VALUE, + name: "Net Debt", + update: ({ position, positionExtra, component, formatting, toBN }) => { + const troveOverallDetails = positionExtra["troveOverallDetails"]; + + const netDebt = toBN(position.debt).minus( + troveOverallDetails.liquidationReserve + ); + + component.value = `${formatting.formatDecimal(netDebt, 2)} LUSD`; + } + }), + defineStrategyComponent({ + type: StrategyComponentType.HEADING, + name: "Withdraw" + }), + defineStrategyComponent({ + type: StrategyComponentType.VALUE, + name: "Collateral", + update: ({ position, component, formatting }) => { + component.value = `${formatting.formatDecimal(position.collateral, 2)} ETH`; + } + }), + defineStrategyComponent({ + type: StrategyComponentType.VALUE, + name: "Liquidation Reserve", + update: ({ positionExtra, component, formatting }) => { + const troveOverallDetails = positionExtra["troveOverallDetails"]; + component.value = `${formatting.formatDecimal( + troveOverallDetails.liquidationReserve, + 2 + )} LUSD`; + } + }) + ], + + validate: async ({ + position, + positionExtra, + toBN, + getTokenByKey, + dsaBalances + }) => { + const troveOpened = + !toBN(position.collateral).isZero() && !toBN(position.debt).isZero(); + + if (!troveOpened) { + return "You should open new trove first"; + } + + const troveOverallDetails = positionExtra["troveOverallDetails"]; + + const netDebt = toBN(position.debt).minus( + troveOverallDetails.liquidationReserve + ); + + const debtToken = getTokenByKey("lusd"); + const debtTokenBalance = dsaBalances[debtToken.address].balance; + + if (toBN(position.debt).gt(debtTokenBalance)) { + const lackOfBalance = netDebt.minus(debtTokenBalance).toPrecision(6, 0); + return `You need ${lackOfBalance} ${debtToken.symbol} more to close your Trove.`; + } + }, + + spells: async () => { + const setId = 0; + + return [ + { + connector: "LIQUITY-A", + method: "close", + args: [setId] + } + ]; + } +}); diff --git a/core/strategies/protocols/liquity/deposit-and-borrow.ts b/core/strategies/protocols/liquity/deposit-and-borrow.ts new file mode 100644 index 0000000..addab1a --- /dev/null +++ b/core/strategies/protocols/liquity/deposit-and-borrow.ts @@ -0,0 +1,284 @@ +import BigNumber from "bignumber.js"; +import abis from "~/constant/abis"; +import addresses from "~/constant/addresses"; +import { + defineStrategy, + defineStrategyComponent, + StrategyComponentType, + StrategyProtocol +} from "../../helpers"; + +export default defineStrategy({ + protocol: StrategyProtocol.LIQUITY, + name: "Deposit & Borrow", + description: "Deposit collateral & borrow asset in a single txn.", + + details: `

This strategy executes:

+
    +
  • Deposit ETH as collateral
  • +
  • Borrow LUSD as Debt
  • +
`, + + submitText: "Deposit & Borrow", + + author: "Instadapp Team", + + variables: { + collateralTokenKey: "eth", + debtTokenKey: "lusd" + }, + + components: [ + defineStrategyComponent({ + type: StrategyComponentType.INPUT_WITH_TOKEN, + name: "Collateral", + placeholder: ({ component: input }) => + input.token ? `${input.token.symbol} to Deposit` : "", + validate: ({ component: input, dsaBalances, toBN }) => { + if (!input.token) { + return "Collateral token is required"; + } + + if (!input.value) { + return "Collateral amount is required"; + } + + const collateralBalance = toBN( + dsaBalances[input.token.address]?.balance + ); + + if (toBN(collateralBalance).lt(input.value)) { + const collateralBalanceFormatted = collateralBalance.toFixed(2); + return `Your amount exceeds your maximum limit of ${collateralBalanceFormatted} ${input.token.symbol}`; + } + }, + defaults: ({ getTokenByKey, variables }) => ({ + token: getTokenByKey?.(variables.collateralTokenKey) + }) + }), + + defineStrategyComponent({ + type: StrategyComponentType.INPUT_WITH_TOKEN, + name: "Debt", + placeholder: ({ component: input }) => + input.token ? `${input.token.symbol} to Borrow` : "", + validate: ({ component: input, toBN, position, positionExtra }) => { + if (!input.token) { + return "Debt token is required"; + } + + if (!input.value) { + return "Debt amount is required"; + } + + const troveOverallDetails = positionExtra["troveOverallDetails"]; + + const borrowFeeAmount = toBN(input.value) + .times(troveOverallDetails.borrowFee) + .toFixed(); + + const debtInputAmountWithFee = toBN(input.value) + .plus(borrowFeeAmount) + .toFixed(); + + const changedDebt = toBN(position.debt).plus(debtInputAmountWithFee); + + const totalDebt = toBN(changedDebt).plus( + troveOverallDetails.liquidationReserve + ); + + if (totalDebt.isZero()) + return `Minimum total debt requirement is ${troveOverallDetails.minDebt} LUSD`; + + if (totalDebt.lt(troveOverallDetails.minDebt) && totalDebt.gt("0")) { + return `Minimum total debt requirement is ${troveOverallDetails.minDebt} LUSD`; + } + }, + defaults: ({ getTokenByKey, variables }) => ({ + token: getTokenByKey?.(variables.debtTokenKey) + }) + }), + defineStrategyComponent({ + type: StrategyComponentType.HEADING, + name: "Projected Debt Position" + }), + defineStrategyComponent({ + type: StrategyComponentType.STATUS, + name: "Status", + update: ({ position, positionExtra, component, components, toBN }) => { + if ( + toBN(components[0].value).isZero() && + toBN(components[1].value).isZero() + ) { + return; + } + + if (!position && !positionExtra) { + return; + } + + const troveOverallDetails = positionExtra["troveOverallDetails"]; + + component.liquidation = troveOverallDetails.liquidation; + component.status = + toBN(components[0].value).isZero() && + !toBN(components[1].value).isZero() + ? "1.1" + : toBN(components[1].value) + .div(toBN(components[0].value).times(position.price)) + .toFixed(); + } + }), + defineStrategyComponent({ + type: StrategyComponentType.VALUE, + name: "LIQUIDATION PRICE (IN ETH)", + value: "-", + update: ({ + position, + component, + components, + toBN, + formatting, + positionExtra + }) => { + if (!position && !positionExtra) { + return; + } + + const troveOverallDetails = positionExtra["troveOverallDetails"]; + + let liquidationPrice = + toBN(components[0].value).isZero() && + !toBN(components[1].value).isZero() + ? toBN(position.price) + .times("1.1") + .toFixed() + : BigNumber.max( + toBN(components[1].value) + .div(components[0].value) + .div(troveOverallDetails.liquidation), + "0" + ).toFixed(); + + component.value = `${formatting.formatUsdMax( + isNaN(parseInt(liquidationPrice)) ? "0" : liquidationPrice, + position.price + )} / ${formatting.formatUsd(position.price)}`; + } + }) + ], + + validate: async ({ position, positionExtra, components: inputs, toBN }) => { + if (toBN(inputs[0].value).isZero() && toBN(inputs[1].value).isZero()) { + return; + } + const troveOpened = + !toBN(position.collateral).isZero() && !toBN(position.debt).isZero(); + + if (!troveOpened) { + return "You should open new trove first"; + } + + // const troveOverallDetails = positionExtra["troveOverallDetails"]; + + // const status = + // toBN(inputs[0].value).isZero() && !toBN(inputs[1].value).isZero() + // ? toBN("1.1") + // : toBN(inputs[0].value) + // .times(position.price) + // .div(inputs[1].value); + + // console.log(status.toFixed(), troveOverallDetails.liquidation); + + // if (status.gt(toBN(troveOverallDetails.liquidation).minus("0.0001"))) { + // return "Position will liquidate."; + // } + }, + + spells: async ({ + components: inputs, + position, + positionExtra, + getTokenByKey, + convertTokenAmountToWei, + web3, + toBN + }) => { + const troveOverallDetails = positionExtra["troveOverallDetails"]; + const collateralToken = getTokenByKey("eth"); + const collateralInWei = convertTokenAmountToWei( + position.collateral, + collateralToken.decimals + ); + + const depositAmountInWei = convertTokenAmountToWei( + inputs[0].value, + inputs[0].token.decimals + ); + const totalDepositAmountInWei = toBN(depositAmountInWei) + .plus(collateralInWei) + .toFixed(); + + const borrowFeeAmount = toBN(inputs[1].value) + .times(troveOverallDetails.borrowFee) + .toFixed(); + const debtInputAmountWithFee = toBN(inputs[1].value) + .plus(borrowFeeAmount) + .toFixed(); + const changedDebt = toBN(position.debt) + .plus(debtInputAmountWithFee) + .toFixed(); + const borrowAmountInWei = convertTokenAmountToWei( + inputs[1].value, + inputs[1].value.decimals + ); + const totalBorrowAmountInWei = convertTokenAmountToWei( + changedDebt, + inputs[1].value.decimals + ); + + const withdrawAmountInWei = "0"; + const paybackAmountInWei = "0"; + + const liquityInstance = new web3.eth.Contract( + abis.resolver.liquity as any, + addresses.mainnet.resolver.liquity + ); + + const { + upperHint, + lowerHint + } = await liquityInstance.methods + .getTrovePositionHints( + totalDepositAmountInWei.toString(), + totalBorrowAmountInWei.toString(), + 0, + 0 + ) + .call(); + + const getIds = [0, 0, 0, 0]; + const setIds = [0, 0, 0, 0]; + + return [ + { + connector: "LIQUITY-A", + method: "adjust", + args: [ + toBN(troveOverallDetails.borrowFee) + .times("100") + .times("1e18") + .toFixed(), + depositAmountInWei, + withdrawAmountInWei, + borrowAmountInWei, + paybackAmountInWei, + upperHint, + lowerHint, + getIds, + setIds + ] + } + ]; + } +}); diff --git a/core/strategies/protocols/liquity/index.ts b/core/strategies/protocols/liquity/index.ts new file mode 100644 index 0000000..ca32db7 --- /dev/null +++ b/core/strategies/protocols/liquity/index.ts @@ -0,0 +1,10 @@ +import depositAndBorrow from "./deposit-and-borrow" +import paybackAndWithdraw from "./payback-and-withdraw" +import closeTrove from "./close-trove" + +export default [ + depositAndBorrow, + paybackAndWithdraw, + closeTrove, +] + \ No newline at end of file diff --git a/core/strategies/protocols/liquity/payback-and-withdraw.ts b/core/strategies/protocols/liquity/payback-and-withdraw.ts new file mode 100644 index 0000000..6141c53 --- /dev/null +++ b/core/strategies/protocols/liquity/payback-and-withdraw.ts @@ -0,0 +1,275 @@ +import BigNumber from "bignumber.js"; +import abis from "~/constant/abis"; +import addresses from "~/constant/addresses"; +import { + defineStrategy, + defineStrategyComponent, + StrategyComponentType, + StrategyProtocol +} from "../../helpers"; + +export default defineStrategy({ + protocol: StrategyProtocol.LIQUITY, + name: "Payback & Withdraw", + description: "Payback debt & withdraw collateral in a single txn.", + author: "Instadapp Team", + + submitText: "Payback & Withdraw", + + details: `

This strategy executes:

+
    +
  • Payback debt
  • +
  • Withdraw collateral
  • +
`, + + components: [ + defineStrategyComponent({ + type: StrategyComponentType.INPUT_WITH_TOKEN, + name: "Debt", + placeholder: ({ component: input }) => + input.token ? `${input.token.symbol} to Payback` : "", + validate: ({ component: input, toBN, position, positionExtra }) => { + if (!input.token) { + return "Debt token is required"; + } + + if (!input.value) { + return "Debt amount is required"; + } + + const troveOverallDetails = positionExtra["troveOverallDetails"]; + + const borrowFeeAmount = toBN(input.value) + .times(troveOverallDetails.borrowFee) + .toFixed(); + + const debtInputAmountWithFee = toBN(input.value) + .plus(borrowFeeAmount) + .toFixed(); + + const changedDebt = toBN(position.debt).plus(debtInputAmountWithFee); + + const totalDebt = toBN(changedDebt).plus( + troveOverallDetails.liquidationReserve + ); + + if (totalDebt.isZero()) + return `Minimum total debt requirement is ${troveOverallDetails.minDebt} LUSD`; + + if (totalDebt.lt(troveOverallDetails.minDebt) && totalDebt.gt("0")) { + return `Minimum total debt requirement is ${troveOverallDetails.minDebt} LUSD`; + } + }, + defaults: ({ getTokenByKey }) => ({ + token: getTokenByKey?.("lusd") + }) + }), + defineStrategyComponent({ + type: StrategyComponentType.INPUT_WITH_TOKEN, + name: "Collateral", + placeholder: ({ component: input }) => + input.token ? `${input.token.symbol} to Withdraw` : "", + validate: ({ component: input, dsaBalances, toBN }) => { + if (!input.token) { + return "Collateral token is required"; + } + + if (!input.value) { + return "Collateral amount is required"; + } + + const collateralBalance = toBN( + dsaBalances[input.token.address]?.balance + ); + + if (toBN(collateralBalance).lt(input.value)) { + const collateralBalanceFormatted = collateralBalance.toFixed(2); + return `Your amount exceeds your maximum limit of ${collateralBalanceFormatted} ${input.token.symbol}`; + } + }, + defaults: ({ getTokenByKey }) => ({ + token: getTokenByKey?.("eth") + }) + }), + defineStrategyComponent({ + type: StrategyComponentType.HEADING, + name: "Projected Debt Position" + }), + defineStrategyComponent({ + type: StrategyComponentType.STATUS, + name: "Status", + update: ({ position, positionExtra, component, components, toBN }) => { + if (!position && !positionExtra) { + return; + } + const troveOverallDetails = positionExtra["troveOverallDetails"]; + component.liquidation = troveOverallDetails.liquidation; + + if ( + toBN(components[1].value).isZero() || + !components[1].value || + toBN(components[0].value).isZero() || + !components[0].value + ) { + component.status = position.ratio; + } else { + const changedDebt = BigNumber.max(toBN(position.debt).minus(components[0].value), '0') + const changedCollateral = toBN(position.collateral).minus(components[1].value); + + component.status = changedDebt + .div(toBN(changedCollateral).times(position.price)) + .toFixed(); + } + } + }), + defineStrategyComponent({ + type: StrategyComponentType.VALUE, + name: "LIQUIDATION PRICE (IN ETH)", + value: "-", + update: ({ + position, + component, + components, + toBN, + formatting, + positionExtra + }) => { + if (!position && !positionExtra) { + return; + } + + const troveOverallDetails = positionExtra["troveOverallDetails"]; + + let liquidationPrice = "0"; + if ( + toBN(components[1].value).isZero() || + !components[1].value || + toBN(components[0].value).isZero() || + !components[0].value + ) { + liquidationPrice = BigNumber.max( + toBN(position.debt) + .div(position.collateral) + .div(troveOverallDetails.liquidation), + "0" + ).toFixed(); + } else { + + const changedDebt = BigNumber.max(toBN(position.debt).minus(components[0].value), '0') + const changedCollateral = toBN(position.collateral).minus(components[1].value); + + liquidationPrice = BigNumber.max( + toBN(changedDebt) + .div(changedCollateral) + .div(troveOverallDetails.liquidation), + "0" + ).toFixed(); + } + + component.value = `${formatting.formatUsdMax( + isNaN(parseInt(liquidationPrice)) ? "0" : liquidationPrice, + position.price + )} / ${formatting.formatUsd(position.price)}`; + } + }) + ], + + validate: async ({ position, components: inputs, toBN }) => { + if (toBN(inputs[0].value).isZero() && toBN(inputs[1].value).isZero()) { + return; + } + const troveOpened = + !toBN(position.collateral).isZero() && !toBN(position.debt).isZero(); + + if (!troveOpened) { + return "You should open new trove first"; + } + }, + + spells: async ({ + components: inputs, + position, + positionExtra, + getTokenByKey, + convertTokenAmountToWei, + web3, + toBN + }) => { + const troveOverallDetails = positionExtra["troveOverallDetails"]; + const collateralToken = getTokenByKey("eth"); + const collateralInWei = convertTokenAmountToWei( + position.collateral, + collateralToken.decimals + ); + + const withdrawAmountInWei = convertTokenAmountToWei( + inputs[0].value, + inputs[0].token.decimals + ); + const totalDepositAmountInWei = toBN(withdrawAmountInWei) + .plus(collateralInWei) + .toFixed(); + + const borrowFeeAmount = toBN(inputs[1].value) + .times(troveOverallDetails.borrowFee) + .toFixed(); + const debtInputAmountWithFee = toBN(inputs[1].value) + .plus(borrowFeeAmount) + .toFixed(); + const changedDebt = toBN(position.debt) + .plus(debtInputAmountWithFee) + .toFixed(); + const paybackAmountInWei = convertTokenAmountToWei( + inputs[1].value, + inputs[1].value.decimals + ); + const totalBorrowAmountInWei = convertTokenAmountToWei( + changedDebt, + inputs[1].value.decimals + ); + + const depositAmountInWei = "0"; + const borrowAmountInWei = "0"; + + const liquityInstance = new web3.eth.Contract( + abis.resolver.liquity as any, + addresses.mainnet.resolver.liquity + ); + + const { + upperHint, + lowerHint + } = await liquityInstance.methods + .getTrovePositionHints( + totalDepositAmountInWei.toString(), + totalBorrowAmountInWei.toString(), + 0, + 0 + ) + .call(); + + const getIds = [0, 0, 0, 0]; + const setIds = [0, 0, 0, 0]; + + return [ + { + connector: "LIQUITY-A", + method: "adjust", + args: [ + toBN(troveOverallDetails.borrowFee) + .times("100") + .times("1e18") + .toFixed(), + depositAmountInWei, + withdrawAmountInWei, + borrowAmountInWei, + paybackAmountInWei, + upperHint, + lowerHint, + getIds, + setIds + ] + } + ]; + } +}); diff --git a/layouts/default.vue b/layouts/default.vue index a664944..e8cacf4 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -137,7 +137,8 @@ export default defineComponent({ } }, { immediate: true }) - onErrorCaptured(() => { + onErrorCaptured((error) => { + console.error(error) return false }) diff --git a/package.json b/package.json index 76055fd..e543ecd 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@nuxtjs/composition-api": "^0.24.7", "@portis/web3": "^4.0.5", "@tailwindcss/forms": "^0.3.3", + "@tailwindcss/typography": "^0.4.1", "@vueuse/core": "^5.1.4", "@walletconnect/web3-provider": "^1.4.1", "bignumber.js": "^9.0.1", @@ -23,6 +24,8 @@ "dsa-connect": "^0.4.4", "nuxt": "^2.15.7", "qrcode": "^1.4.4", + "slugify": "^1.6.0", + "tiny-emitter": "^2.1.0", "v-click-outside": "^3.1.2", "v-tooltip": "^2.1.3", "vue-clipboard2": "^0.3.1", diff --git a/pages/aave-v2.vue b/pages/aave-v2.vue index e1c64c0..3b00a21 100644 --- a/pages/aave-v2.vue +++ b/pages/aave-v2.vue @@ -10,18 +10,43 @@ -
-
+
+
- +
+ +
+

Aave v2

-

Aave v2

+ + + Strategies + + + + +
@@ -166,12 +191,14 @@ import { useStatus } from "~/composables/useStatus"; import { useBigNumber } from "~/composables/useBigNumber"; import CardAave from "~/components/protocols/CardAave.vue"; import AaveIcon from "~/assets/icons/aave.svg?inline"; +import ButtonCTAOutlined from "~/components/common/input/ButtonCTAOutlined.vue"; export default defineComponent({ components: { BackIcon, CardAave, - AaveIcon + AaveIcon, + ButtonCTAOutlined, }, setup() { const { diff --git a/pages/mainnet/aave-v2.vue b/pages/mainnet/aave-v2.vue deleted file mode 100644 index 46501dc..0000000 --- a/pages/mainnet/aave-v2.vue +++ /dev/null @@ -1,216 +0,0 @@ - - - diff --git a/pages/mainnet/compound.vue b/pages/mainnet/compound.vue index d4e8f2c..a8d6372 100644 --- a/pages/mainnet/compound.vue +++ b/pages/mainnet/compound.vue @@ -10,18 +10,43 @@
-
-
+
+
- +
+ +
+

Compound

-

Compound

+ + + Strategies + + + + +
@@ -163,13 +188,15 @@ import { useSearchFilter } from "~/composables/useSearchFilter"; import { useStatus } from "~/composables/useStatus"; import { useBigNumber } from "~/composables/useBigNumber"; import CardCompound from "~/components/protocols/CardCompound.vue"; -import CompoundIcon from '~/assets/icons/compound.svg?inline' +import CompoundIcon from "~/assets/icons/compound.svg?inline"; +import ButtonCTAOutlined from "~/components/common/input/ButtonCTAOutlined.vue"; export default defineComponent({ components: { BackIcon, CardCompound, CompoundIcon, + ButtonCTAOutlined, }, setup() { const { diff --git a/pages/mainnet/liquity.vue b/pages/mainnet/liquity.vue index c6b0714..b945dc6 100644 --- a/pages/mainnet/liquity.vue +++ b/pages/mainnet/liquity.vue @@ -10,18 +10,43 @@
-
-
+
+
- +
+ +
+

Liquity

-

Liquity

+ + + Strategies + + + + +
@@ -150,6 +175,7 @@ import { useFormatting } from "~/composables/useFormatting"; import { useStatus } from "~/composables/useStatus"; import { useBigNumber } from "~/composables/useBigNumber"; import CardLiquityTrove from "~/components/protocols/liquity/CardLiquityTrove.vue"; +import ButtonCTAOutlined from "~/components/common/input/ButtonCTAOutlined.vue"; export default defineComponent({ components: { @@ -159,7 +185,8 @@ export default defineComponent({ SVGAdd, SVGBalance, SVGPercent, - CardLiquityTrove + CardLiquityTrove, + ButtonCTAOutlined, }, setup() { const router = useRouter(); diff --git a/tailwind.config.js b/tailwind.config.js index 240fc3e..f6a40cf 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -120,5 +120,6 @@ module.exports = { }, plugins: [ require('@tailwindcss/forms'), + require('@tailwindcss/typography'), ], } diff --git a/yarn.lock b/yarn.lock index b9a7cbc..58d047a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1727,6 +1727,16 @@ dependencies: mini-svg-data-uri "^1.2.3" +"@tailwindcss/typography@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@tailwindcss/typography/-/typography-0.4.1.tgz#51ddbceea6a0ee9902c649dbe58871c81a831212" + integrity sha512-ovPPLUhs7zAIJfr0y1dbGlyCuPhpuv/jpBoFgqAc658DWGGrOBWBMpAWLw2KlzbNeVk4YBJMzue1ekvIbdw6XA== + dependencies: + lodash.castarray "^4.4.0" + lodash.isplainobject "^4.0.6" + lodash.merge "^4.6.2" + lodash.uniq "^4.5.0" + "@types/anymatch@*": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-3.0.0.tgz#c95ff14401dbb2869913afac3935af4ad0d37f1a" @@ -7297,11 +7307,21 @@ lodash._reinterpolate@^3.0.0: resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= +lodash.castarray@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.castarray/-/lodash.castarray-4.4.0.tgz#c02513515e309daddd4c24c60cfddcf5976d9115" + integrity sha1-wCUTUV4wna3dTCTGDP3c9ZdtkRU= + lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= + lodash.kebabcase@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36" @@ -7312,6 +7332,11 @@ lodash.memoize@^4.1.2: resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + lodash.template@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" @@ -10430,6 +10455,11 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +slugify@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.6.0.tgz#6bdf8ed01dabfdc46425b67e3320b698832ff893" + integrity sha512-FkMq+MQc5hzYgM86nLuHI98Acwi3p4wX+a5BO9Hhw4JdK4L7WueIiZ4tXEobImPqBz2sVcV0+Mu3GRB30IGang== + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -11045,7 +11075,7 @@ timsort@^0.3.0: resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= -tiny-emitter@^2.0.0: +tiny-emitter@^2.0.0, tiny-emitter@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==