Liquity - Open Trove

This commit is contained in:
Georges KABBOUCHI 2021-08-21 14:20:16 +03:00
parent 5d213e4ad7
commit 645bf1ecbf
13 changed files with 1309 additions and 12 deletions

View File

@ -0,0 +1,29 @@
<template>
<button
class="flex items-center justify-center px-4 font-semibold leading-none transition-colors duration-150 border border-dashed cursor-pointer focus:outline-none rounded-[4px]"
:class="{
'w-full': fullWidth,
'text-ocean-blue-pure dark:text-white border-ocean-blue-pure dark:border-grey-pure bg-ocean-blue-light border-opacity-25 bg-opacity-38 dark:bg-opacity-17 dark:hover:bg-opacity-25 hover:bg-opacity-75 active:bg-opacity-38 dark:active:bg-opacity-38 ':
color === 'ocean-blue',
}"
:style="`height: ${height}`"
v-bind="$attrs"
v-on="$listeners"
>
<slot></slot>
</button>
</template>
<script>
import { defineComponent } from '@nuxtjs/composition-api'
export default defineComponent({
props: {
color: { type: String, default: 'grey' },
height: { type: String, default: '60px' },
fullWidth: { type: Boolean, default: false },
},
})
</script>
<style></style>

View File

@ -0,0 +1,155 @@
<template>
<div class="flex flex-col flex-shrink-0 w-full">
<div class="relative transition-all duration-150 rounded-sm" :class="{ 'shadow-sm': !disabled }">
<input
autocomplete="off"
type="text"
inputmode="decimal"
:value="value"
v-bind="$attrs"
class="w-full pl-4 rounded-[6px] transition-colors duration-75 ease-out border border-grey-dark border-opacity-[0.15]"
:class="{
'pr-12': !!symbol,
'pr-8': !symbol,
'rounded-b-none': badgeValue != null,
'text-sm': size === 'lg',
'bg-gray-50': backgroundColor === 'grey',
}"
:aria-invalid="(touched || showError) && !!error"
:aria-describedby="id"
:disabled="disabled"
v-on="inputListeners"
/>
<div v-if="!!tokenKeys && !!tokenKeys.length" class="absolute inset-y-0 right-0">
<Dropdown class="h-full">
<template #trigger="{ toggle }">
<button
v-if="!!symbol"
class="flex items-center h-full px-1 font-semibold whitespace-no-wrap text-ocean-blue-pure text-11 dark:text-light"
@click="toggle"
>
<span class="mr-1">{{ symbol }}</span>
<Icon name="chevron-down" class="h-3" />
</button>
</template>
<template #menu="{ close }">
<DropdownMenu>
<button
v-for="(tokenKey, index) in tokenKeys"
:key="index"
class="flex items-center w-full px-4 py-2 font-semibold text-ocean-blue-pure text-11 dark:text-light hover:bg-opacity-50 focus:bg-opacity-50 hover:bg-grey-light dark:hover:bg-dark-300 dark:focus:bg-dark-300 focus:outline-none focus:bg-grey-light"
@click="select(tokenKey, close)"
>
{{ getSymbol(tokenKey) }}
</button>
</DropdownMenu>
</template>
</Dropdown>
</div>
<div
v-else-if="!!getSymbol(tokenKey)"
class="absolute inset-y-0 right-0 flex items-center pr-4 pointer-events-none select-none"
>
<span
class="uppercase"
:class="{
'text-ocean-blue-pure dark:text-light': !disabled,
'text-grey-pure': disabled,
'text-11': size === 'md',
'text-14 font-medium': size === 'lg',
}"
>
{{ getSymbol(tokenKey) }}
</span>
</div>
</div>
<Badge
v-if="badgeValue != null"
class="h-6 rounded-t-none rounded-b-sm bg-opacity-38 bg-grey-pure text-opacity-38"
large
>
<SVGSpinner v-if="badgeLoading" class="animate-spin-loading" style="width: auto; height: 1em" />
<span v-else>{{ badgeValue }}</span>
</Badge>
<div class="h-4">
<transition
enter-active-class="duration-75 ease-out"
enter-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="duration-75 ease-in"
leave-class="opacity-100"
leave-to-class="opacity-0"
>
<p v-if="(touched || showError) && !!error" :id="id" class="mt-1 text-red-600 text-12">
{{ error }}
</p>
</transition>
</div>
</div>
</template>
<script>
import { defineComponent, watch, ref, toRef } from '@nuxtjs/composition-api'
import { useInputListeners } from '@/composables/useInputListeners'
import SVGSpinner from '@/assets/img/icons/spinner.svg'
import { v4 as uuid } from 'uuid'
import { useToken } from '~/composables/useToken'
import { useFormatting } from '~/composables/useFormatting'
import { usePattern } from '~/composables/usePattern'
import Dropdown from './Dropdown.vue'
import DropdownMenu from '~/components/protocols/DropdownMenu.vue'
export default defineComponent({
inheritAttrs: false,
components: {
SVGSpinner,
Dropdown,
DropdownMenu,
},
props: {
value: { type: String, default: '' },
badgeValue: { type: String, default: null },
badgeLoading: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
showError: { type: Boolean, default: false },
error: { type: String, default: null },
tokenKey: { type: String, required: true },
tokenKeys: { type: Array, default: () => null },
size: { type: String, default: 'md' },
backgroundColor: { type: String, default: 'white' },
},
setup(props, context) {
const id = uuid()
const { symbol, getTokenByKey } = useToken(null, toRef(props, 'tokenKey'))
const { formatDecimal } = useFormatting()
const { amountPattern } = usePattern()
const numericFilter = (value) => amountPattern.test(value)
const { inputListeners } = useInputListeners(props, context, numericFilter)
const touched = ref(false)
const stopTouchedWatcher = watch(
() => props.value,
() => {
touched.value = true
stopTouchedWatcher()
}
)
function getSymbol(key) {
return getTokenByKey(key)?.symbol || key
}
function select(tokenKey, cb) {
context.emit('tokenKeyChanged', tokenKey)
cb()
}
return { inputListeners, touched, symbol, formatDecimal, id, getSymbol, select }
},
})
</script>

View File

@ -0,0 +1,253 @@
<template>
<SidebarContextRootContainer>
<template #title>Open Trove</template>
<SidebarSectionValueWithIcon label="Collateral Balance" center>
<template #icon
><IconCurrency
:currency="collateralToken.key"
class="w-16 h-16"
noHeight
/></template>
<template #value
>{{ formatNumber(balance) }} {{ collateralToken.symbol }}</template
>
</SidebarSectionValueWithIcon>
<div class="bg-[#C5CCE1] bg-opacity-[0.15] mt-10 p-8">
<input-amount
v-model="collateralAmount"
:token-key="collateralToken.key"
:disabled="pending"
class="mt-4"
placeholder="Collateral amount"
:error="errors.collateralAmount.message"
/>
<input-amount
v-model="debtAmount"
:token-key="debtToken.key"
:disabled="pending"
class="mt-4"
placeholder="Borrow amount"
:error="errors.debtAmount.message"
/>
<ValueDisplay
label="Liquidation Reserve"
tooltip="An amount set aside to cover the liquidators gas costs if your Trove needs to be liquidated. The amount increases your debt and is refunded if you close your Trove by fully paying off its net debt."
class="mt-4"
>
{{ liquidationReserve }} LUSD
</ValueDisplay>
<ValueDisplay
label="Borrow Fee"
tooltip="This amount is deducted from the borrowed amount as a one-time fee. There are no recurring fees for borrowing, which is thus interest-free."
class="mt-4"
>
<div class="flex items-center">
<div>
{{ formatDecimal(borrowFeeAmount, 2) }} {{ debtToken.symbol }}
</div>
<div class="ml-1 text-sm">({{ formatPercent(borrowFee) }})</div>
</div>
</ValueDisplay>
<ValueDisplay
label="Total debt"
tooltip="The total amount of LUSD your Trove will hold."
class="mt-4"
>
<div>{{ formatDecimal(totalDebt, 2) }} {{ debtToken.symbol }}</div>
</ValueDisplay>
<SidebarContextHeading class="mt-5">
Projected Debt Position
</SidebarContextHeading>
<SidebarSectionStatus
class="mt-8"
:liquidation="liquidation"
:status="status"
/>
<SidebarSectionValueWithIcon class="mt-8" label="Liquidation Price (ETH)">
<template #value>
{{ formatUsdMax(liquidationPrice, liquidationMaxPrice) }}
<span class="text-primary-gray"
>/ {{ formatUsd(liquidationMaxPrice) }}</span
>
</template>
</SidebarSectionValueWithIcon>
<div class="flex flex-shrink-0 mt-10">
<ButtonCTA
class="w-full"
:disabled="!isValid || pending"
:loading="pending"
@click="cast"
>
Open Trove
</ButtonCTA>
</div>
<ValidationErrors :error-messages="errorMessages" class="mt-6" />
</div>
</SidebarContextRootContainer>
</template>
<script>
import { computed, defineComponent, ref } from '@nuxtjs/composition-api'
import InputNumeric from '~/components/common/input/InputNumeric.vue'
import { useBalances } from '~/composables/useBalances'
import { useNotification } from '~/composables/useNotification'
import { useBigNumber } from '~/composables/useBigNumber'
import { useFormatting } from '~/composables/useFormatting'
import { useValidators } from '~/composables/useValidators'
import { useValidation } from '~/composables/useValidation'
import { useToken } from '~/composables/useToken'
import { useParsing } from '~/composables/useParsing'
import { useMaxAmountActive } from '~/composables/useMaxAmountActive'
import { useWeb3 } from '~/composables/useWeb3'
import atokens from '~/constant/atokens'
import ToggleButton from '~/components/common/input/ToggleButton.vue'
import { useDSA } from '~/composables/useDSA'
import ButtonCTA from '~/components/common/input/ButtonCTA.vue'
import Button from '~/components/Button.vue'
import { useSidebar } from '~/composables/useSidebar'
import { useLiquityPosition } from '~/composables/protocols/useLiquityPosition'
import InputAmount from '~/components/common/input/InputAmount.vue'
import ValueDisplay from '../components/ValueDisplay.vue'
export default defineComponent({
components: { InputNumeric, ToggleButton, ButtonCTA, Button, InputAmount, ValueDisplay },
setup() {
const { account } = useWeb3()
const { close } = useSidebar()
const { formatPercent, formatNumber, formatDecimal, formatUsdMax, formatUsd } = useFormatting()
const { plus, times, isZero } = useBigNumber()
const { parseSafeFloat } = useParsing()
const { getBalanceByKey } = useBalances()
const { valInt } = useToken()
const { showPendingTransaction, showWarning } = useNotification()
const { dsa } = useDSA()
const {
collateralToken,
debtToken,
liquidation,
liquidationReserve,
liquidationMaxPrice,
borrowFee,
maxFeePercentageInWei,
getTrovePositionHints,
} = useLiquityPosition()
const collateralAmount = ref('')
const debtAmount = ref('')
const balance = computed(() => getBalanceByKey(collateralToken.value.key))
const collateralAmountParsed = computed(() => parseSafeFloat(collateralAmount.value))
const debtAmountParsed = computed(() => parseSafeFloat(debtAmount.value))
const borrowFeeAmount = computed(() => times(debtAmountParsed.value, borrowFee.value).toFixed())
const totalDebt = computed(() => {
if (isZero(debtAmountParsed.value)) return '0'
return plus(plus(debtAmountParsed.value, borrowFeeAmount.value), liquidationReserve.value).toFixed()
})
const { liquidationPrice, status } = useLiquityPosition(collateralAmountParsed, totalDebt)
const { validateAmount, validateLiquidation, validateIsLoggedIn, validateLiquityDebt } = useValidators()
const errors = computed(() => {
const hasCollateralAmountValue = !isZero(collateralAmount.value)
const hasDebtAmountValue = !isZero(debtAmount.value)
return {
collateralAmount: {
message: validateAmount(collateralAmountParsed.value, balance.value),
show: hasCollateralAmountValue,
},
debtAmount: { message: validateAmount(debtAmountParsed.value), show: hasDebtAmountValue },
minDebt: { message: validateLiquityDebt(totalDebt.value, undefined, '0'), show: hasDebtAmountValue },
liquidation: {
message: validateLiquidation(status.value, liquidation.value),
show: hasCollateralAmountValue && hasDebtAmountValue,
},
auth: { message: validateIsLoggedIn(!!account.value), show: true },
}
})
const { errorMessages, isValid } = useValidation(errors)
const pending = ref(false)
async function cast() {
pending.value = true
try {
const depositAmountInWei = valInt(collateralAmountParsed.value, collateralToken.value.decimals)
const borrowAmountInWei = valInt(debtAmountParsed.value, debtToken.value.decimals)
const totalBorrowAmountInWei = valInt(totalDebt.value, debtToken.value.decimals)
const { upperHint, lowerHint } = await getTrovePositionHints(depositAmountInWei, totalBorrowAmountInWei)
const spells = dsa.value.Spell()
const getIds = [0, 0]
const setIds = [0, 0]
spells.add({
connector: 'LIQUITY-A',
method: 'open',
args: [
depositAmountInWei,
maxFeePercentageInWei.value,
borrowAmountInWei,
upperHint,
lowerHint,
getIds,
setIds,
],
})
const txHash = await dsa.value.cast({
spells,
from: account.value,
})
showPendingTransaction(txHash)
} catch (error) {
console.log(error)
showWarning(error.message)
}
pending.value = false
close()
}
return {
formatPercent, formatNumber, formatDecimal, formatUsdMax, formatUsd,
balance,
liquidationPrice,
liquidationMaxPrice,
status,
liquidation,
totalDebt,
liquidationReserve,
collateralToken,
debtToken,
borrowFee,
borrowFeeAmount,
collateralAmount,
debtAmount,
errors,
errorMessages,
isValid,
pending,
cast,
}
},
})
</script>

View File

@ -0,0 +1,300 @@
import { computed, Ref, ref, watch } from "@nuxtjs/composition-api";
import { useBalances } from "../useBalances";
import { useBigNumber } from "../useBigNumber";
import { useToken } from "../useToken";
import { useWeb3 } from "~/composables/useWeb3";
import { AbiItem } from "web3-utils";
import BigNumber from "bignumber.js";
BigNumber.config({ POW_PRECISION: 200 });
import abis from "~/constant/abis";
import addresses from "~/constant/addresses";
import { useDSA } from "../useDSA";
const trove = ref<any>({
collateral: "0",
debt: "0",
stabilityAmount: "0",
stabilityEthGain: "0",
stabilityLqtyGain: "0",
stakeAmount: "0",
stakeEthGain: "0",
stakeLqtyGain: "0",
price: "0",
ratio: "0",
tokenKey: "eth",
token: "ETH",
liquidation: "0"
});
const troveTypes = ref([
{
totalCollateral: "0",
price: "0",
totalRatio: "0",
tokenKey: "eth",
token: "ETH",
isRecoveryMode: false,
borrowFee: "0",
liquidation: "0",
minDebt: "2000",
liquidationReserve: "200"
}
]);
const troveOverallDetails = computed(() =>
troveTypes.value.find(t => t.tokenKey === trove.value.tokenKey)
);
export function useLiquityPosition(
collateralAmountRef: Ref = null,
debtAmountRef: Ref = null
) {
const { web3 } = useWeb3();
const { activeAccount } = useDSA();
const { isZero, times, div, max, minus, plus } = useBigNumber();
const { getTokenByKey, valInt } = useToken();
const { prices } = useBalances();
const collateralToken = computed(() => getTokenByKey("eth"));
const debtToken = computed(() => getTokenByKey("lusd"));
const stakingToken = computed(() => getTokenByKey("lqty"));
const collateral = computed(() => trove.value.collateral);
const collateralInWei = computed(() =>
valInt(collateral.value, collateralToken.value?.decimals)
);
const priceInUsd = computed(() => trove.value.price);
const ratio = computed(() => trove.value.ratio);
const debt = computed(() => trove.value.debt);
const collateralUsd = computed(() =>
times(collateral.value, priceInUsd.value).toFixed()
);
const stabilityAmount = computed(() => trove.value.stabilityAmount);
const debtUsd = computed(() => times(debt.value, "1").toFixed());
const stabilityAmountUsd = computed(() =>
times(stabilityAmount.value, "1").toFixed()
);
const stakingTokenPrice = computed(() =>
stakingToken.value ? prices.mainnet[stakingToken.value.address] : "0"
);
const stakeAmount = computed(() => trove.value.stakeAmount);
const stakeEthGain = computed(() => trove.value.stakeEthGain);
const stakeLqtyGain = computed(() => trove.value.stakeLqtyGain);
const stakingAmountUsd = computed(() =>
times(stakeAmount.value, stakingTokenPrice.value).toFixed()
);
const netValue = computed(() =>
plus(
plus(minus(collateralUsd.value, debtUsd.value), stabilityAmountUsd.value),
stakingAmountUsd.value
).toFixed()
);
const borrowFee = computed(() => troveOverallDetails.value.borrowFee);
const maxFeePercentageInWei = computed(() =>
times(times(borrowFee.value, "100"), "1e18").toFixed()
);
const liquidation = computed(() => troveOverallDetails.value.liquidation);
const status = computed(() => {
if (!collateralAmountRef || !debtAmountRef) return ratio.value;
return isZero(collateralAmountRef.value) && !isZero(debtAmountRef.value)
? "1.1"
: div(
debtAmountRef.value,
times(collateralAmountRef.value, priceInUsd.value)
).toFixed();
});
const liquidationPrice = computed(() => {
if (!collateralAmountRef || !debtAmountRef) {
return max(
div(div(debt.value, collateral.value), liquidation.value),
"0"
).toFixed();
}
return isZero(collateralAmountRef.value) && !isZero(debtAmountRef.value)
? times(priceInUsd.value, "1.1").toFixed()
: max(
div(
div(debtAmountRef.value, collateralAmountRef.value),
liquidation.value
),
"0"
).toFixed();
});
const troveOpened = computed(
() => !isZero(collateral.value) && !isZero(debt.value)
);
const minDebt = computed(() => troveOverallDetails.value.minDebt);
const liquidationReserve = computed(
() => troveOverallDetails.value.liquidationReserve
);
const fetchPosition = async () => {
if (!web3.value) {
return;
}
troveTypes.value = await getTroveTypes(web3.value);
if (!activeAccount.value) {
return;
}
trove.value = await getTrove(activeAccount.value.address, web3.value);
};
async function getTrovePositionHints(collateralInWei, debtInWei) {
try {
const liquityInstance = new web3.value.eth.Contract(
abis.resolver.liquity as AbiItem[],
addresses.mainnet.resolver.liquity
);
const {
upperHint,
lowerHint
} = await liquityInstance.methods
.getTrovePositionHints(
collateralInWei.toString(),
debtInWei.toString(),
0,
0
)
.call();
return {
upperHint,
lowerHint
};
} catch (error) {
return Promise.reject(error);
}
}
watch(
web3,
async val => {
if (val) {
fetchPosition();
}
},
{ immediate: true }
);
watch(
activeAccount,
async val => {
if (val) {
fetchPosition();
}
},
{ immediate: true }
);
return {
troveOpened,
netValue,
borrowFee,
status,
liquidation,
liquidationPrice,
liquidationMaxPrice: priceInUsd,
collateralToken,
debtToken,
minDebt,
liquidationReserve,
maxFeePercentageInWei,
getTrovePositionHints
};
}
async function getTrove(user, web3) {
const resolveABI = abis.resolver.liquity;
const resolveAddr = addresses.mainnet.resolver.liquity;
const liquityInstance = new web3.eth.Contract(
resolveABI as AbiItem[],
resolveAddr
);
try {
const {
trove,
stake,
stability
} = await liquityInstance.methods.getPosition(user).call();
const { collateral, debt, icr, price } = trove;
const ratio =
icr ===
"115792089237316195423570985008687907853269984665640564039457584007913129639935"
? "0"
: new BigNumber(1e18).dividedBy(icr).toString();
return {
collateral: new BigNumber(collateral).dividedBy(1e18).toString(),
debt: new BigNumber(debt).dividedBy(1e18).toString(),
stabilityAmount: new BigNumber(stability.deposit)
.dividedBy(1e18)
.toString(),
stabilityEthGain: new BigNumber(stability.ethGain)
.dividedBy(1e18)
.toString(),
stabilityLqtyGain: new BigNumber(stability.lqtyGain)
.dividedBy(1e18)
.toString(),
stakeAmount: new BigNumber(stake.amount).dividedBy(1e18).toString(),
stakeEthGain: new BigNumber(stake.ethGain).dividedBy(1e18).toString(),
stakeLqtyGain: new BigNumber(stake.lusdGain).dividedBy(1e18).toString(),
price: new BigNumber(price).dividedBy(1e18).toString(),
ratio,
tokenKey: "eth",
token: "ETH",
liquidation: ratio
};
} catch (error) {
console.error(error);
return {};
}
}
async function getTroveTypes(web3) {
try {
const resolveABI = abis.resolver.liquity;
const resolveAddr = addresses.mainnet.resolver.liquity;
const liquityInstance = new web3.eth.Contract(
resolveABI as AbiItem[],
resolveAddr
);
const {
borrowFee,
ethTvl,
isInRecoveryMode: isRecoveryMode,
tcr,
price
} = await liquityInstance.methods.getSystemState().call();
return [
{
totalCollateral: new BigNumber(ethTvl).dividedBy(1e18).toString(),
price: new BigNumber(price).dividedBy(1e18).toString(),
totalRatio: new BigNumber(1e18).dividedBy(tcr).toString(),
tokenKey: "eth",
token: "ETH",
isRecoveryMode,
borrowFee: new BigNumber(borrowFee).dividedBy(1e18).toString(),
liquidation: new BigNumber(100).dividedBy(110).toString(),
minDebt: new BigNumber(2000).toString(),
liquidationReserve: "200"
}
];
} catch (error) {
return [];
}
}

View File

@ -29,6 +29,9 @@ import SidebarMakerdaoWithdraw from '~/components/sidebar/context/makerdao/Sideb
import SidebarMakerdaoBorrow from '~/components/sidebar/context/makerdao/SidebarMakerdaoBorrow.vue'
import SidebarMakerdaoPayback from '~/components/sidebar/context/makerdao/SidebarMakerdaoPayback.vue'
import SidebarLiquityTroveOpenNew from '~/components/sidebar/context/liquity/SidebarLiquityTroveOpenNew.vue'
const sidebars = {
"#overview" : {component: SidebarOverview, back : false, close : true },
"#deposit-overview": {component: SidebarDepositOverview, back: { hash: 'overview' } },
@ -59,6 +62,8 @@ const sidebars = {
"/mainnet/maker#withdraw": { component: SidebarMakerdaoWithdraw },
"/mainnet/maker#borrow": { component: SidebarMakerdaoBorrow },
"/mainnet/maker#payback": { component: SidebarMakerdaoPayback },
'/mainnet/liquity#trove-new': { component: SidebarLiquityTroveOpenNew },
};
const sidebar = ref(null);

View File

@ -1,11 +1,17 @@
import { useBigNumber } from "./useBigNumber";
import { useFormatting } from "./useFormatting";
import { useMakerdaoPosition } from "~/composables/protocols/useMakerdaoPosition";
import { useLiquityPosition } from "./protocols/useLiquityPosition";
export function useValidators() {
const { formatNumber } = useFormatting();
const { isZero, minus, eq, gt, lt, gte, plus } = useBigNumber();
const { minDebt: makerMinDebt, vaultTypes } = useMakerdaoPosition();
const {
minDebt: liquityMinDebt,
liquidationReserve: liquityLiquidationReserve,
troveOpened: liquityTroveOpened
} = useLiquityPosition();
function validateAmount(amountParsed, balance = null, options = null) {
const mergedOptions = Object.assign(
@ -90,12 +96,39 @@ export function useValidators() {
return null;
}
function validateLiquityDebt(
debtParsed,
minDebt = liquityMinDebt.value,
liquidationReserve = liquityLiquidationReserve.value
) {
const totalDebt = plus(debtParsed, liquidationReserve);
if (isZero(totalDebt))
return `Minimum total debt requirement is ${minDebt} LUSD`;
if (lt(totalDebt, minDebt) && gt(totalDebt, "0")) {
return `Minimum total debt requirement is ${minDebt} LUSD`;
}
return null;
}
function validateLiquityTroveExists() {
if (!liquityTroveOpened.value) {
return "You should open new trove first";
}
return null;
}
return {
validateAmount,
validateLiquidation,
validateIsLoggedIn,
validateLiquidity,
validateMakerDebt,
validateMakerDebtCeiling
validateMakerDebtCeiling,
validateLiquityDebt,
validateLiquityTroveExists
};
}

View File

@ -0,0 +1,361 @@
[
{
"inputs": [],
"name": "fetchETHPrice",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "getPosition",
"outputs": [
{
"components": [
{
"components": [
{
"internalType": "uint256",
"name": "collateral",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "debt",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "icr",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "price",
"type": "uint256"
}
],
"internalType": "struct Helpers.Trove",
"name": "trove",
"type": "tuple"
},
{
"components": [
{
"internalType": "uint256",
"name": "deposit",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "ethGain",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "lqtyGain",
"type": "uint256"
}
],
"internalType": "struct Helpers.StabilityDeposit",
"name": "stability",
"type": "tuple"
},
{
"components": [
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "ethGain",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "lusdGain",
"type": "uint256"
}
],
"internalType": "struct Helpers.Stake",
"name": "stake",
"type": "tuple"
}
],
"internalType": "struct Helpers.Position",
"name": "",
"type": "tuple"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "searchIterations",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "randomSeed",
"type": "uint256"
}
],
"name": "getRedemptionPositionHints",
"outputs": [
{
"internalType": "uint256",
"name": "partialHintNicr",
"type": "uint256"
},
{
"internalType": "address",
"name": "firstHint",
"type": "address"
},
{
"internalType": "address",
"name": "upperHint",
"type": "address"
},
{
"internalType": "address",
"name": "lowerHint",
"type": "address"
},
{
"internalType": "uint256",
"name": "oracleEthPrice",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "getStabilityDeposit",
"outputs": [
{
"components": [
{
"internalType": "uint256",
"name": "deposit",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "ethGain",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "lqtyGain",
"type": "uint256"
}
],
"internalType": "struct Helpers.StabilityDeposit",
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "getStake",
"outputs": [
{
"components": [
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "ethGain",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "lusdGain",
"type": "uint256"
}
],
"internalType": "struct Helpers.Stake",
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "getSystemState",
"outputs": [
{
"components": [
{
"internalType": "uint256",
"name": "borrowFee",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "ethTvl",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "tcr",
"type": "uint256"
},
{
"internalType": "bool",
"name": "isInRecoveryMode",
"type": "bool"
},
{
"internalType": "uint256",
"name": "price",
"type": "uint256"
}
],
"internalType": "struct Helpers.System",
"name": "",
"type": "tuple"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "getTrove",
"outputs": [
{
"components": [
{
"internalType": "uint256",
"name": "collateral",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "debt",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "icr",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "price",
"type": "uint256"
}
],
"internalType": "struct Helpers.Trove",
"name": "",
"type": "tuple"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "collateral",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "debt",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "searchIterations",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "randomSeed",
"type": "uint256"
}
],
"name": "getTrovePositionHints",
"outputs": [
{
"internalType": "address",
"name": "upperHint",
"type": "address"
},
{
"internalType": "address",
"name": "lowerHint",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
}
]

View File

@ -6,6 +6,7 @@ import compoundABI from "./abi/read/compound.json";
import makerABI from "./abi/read/maker.json";
import makerProxyRegistryABI from "./abi/makerProxyRegistry.json";
import unipoolABI from "./abi/read/unipool.json";
import liquityABI from "./abi/read/liquity.json";
const abis = {
makerProxyRegistry: makerProxyRegistryABI,
@ -17,7 +18,8 @@ const abis = {
compound: compoundABI,
maker: makerABI,
unipool: unipoolABI,
},
liquity: liquityABI,
}
};
export default abis;

View File

@ -9,6 +9,7 @@ const addresses = {
compound: "0xcCAa4b1b3931749b8b6EF19C6b0B2c496703321b",
maker: "0x84addce4fac0b6ee4b0cd132120d6d4b700e35c0",
unipool: "0x22bddA39D14eD0aafeee36B6e784602fdDE64723",
liquity: '0xDAf2A39503463B0F41f899EDD82213b3c96b6Cf8',
},
},

View File

@ -12,6 +12,7 @@ export default {
{ key: 'mkr', type: 'token', symbol: 'MKR', name: 'MakerDAO', address: '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2', decimals: 18, isStableCoin: false},
{ key: 'comp', type: 'token', symbol: 'COMP', name: 'Compound', address: '0xc00e94Cb662C3520282E6f5717214004A7f26888', decimals: 18, isStableCoin: false},
{ key: 'rai', type: 'token', symbol: 'RAI', name: 'Rai Reflex Index', address: '0x03ab458634910AaD20eF5f1C8ee96F1D6ac54919', decimals: 18, isStableCoin: false},
{ key: 'lusd', type: 'token', symbol: 'LUSD', name: 'Liquity USD', address: '0x5f98805A4E8be255a32880FDeC7F6728C6568bA0', decimals: 18, isStableCoin: true},
{ key: 'zrx', type: 'token', symbol: 'ZRX', name: '0x Protocol', address: '0xE41d2489571d322189246DaFA5ebDe1F4699F498', decimals: 18, isStableCoin: false},
{ key: 'rep', type: 'token', symbol: 'REP', name: 'Augur', address: '0x1985365e9f78359a9B6AD760e32412f4a445E862', decimals: 18, isStableCoin: false},
{ key: 'tusd', type: 'token', symbol: 'TUSD', name: 'TrueUSD', address: '0x0000000000085d4780B73119b644AE5ecd22b376', decimals: 18, isStableCoin: true},
@ -44,7 +45,8 @@ export default {
{ key: 'eth2xfli', type: 'token', symbol: 'ETH2x-FLI', name: 'ETH 2x Flexible Leverage Index', address: '0xAa6E8127831c9DE45ae56bB1b0d4D4Da6e5665BD', decimals: 18, isStableCoin: false, },
{ key: 'btc2xfli', type: 'token', symbol: 'BTC2x-FLI', name: 'BTC 2x Flexible Leverage Index', address: '0x0B498ff89709d3838a063f1dFA463091F9801c2b', decimals: 18, isStableCoin: false, },
{ key: 'stkaave', type: 'token', symbol: 'stkAAVE', name: 'Staked Aave', address: '0x4da27a545c0c5B758a6BA100e3a049001de870f5', decimals: 18, isStableCoin: false, },
{ key: 'matic', type: 'token', symbol: 'MATIC', name: 'Matic Token', address: '0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0', decimals: 18, isStableCoin: false, }
{ key: 'matic', type: 'token', symbol: 'MATIC', name: 'Matic Token', address: '0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0', decimals: 18, isStableCoin: false, },
{ key: 'lqty', type: 'token', symbol: 'LQTY', name: 'LQTY', address: '0x6DEA81C8171D0bA574754EF6F8b412F2Ed88c54D', decimals: 18, isStableCoin: false},
]),
polygon: createTokenUtils([

View File

@ -18,7 +18,7 @@
"bignumber.js": "^9.0.1",
"core-js": "^3.15.1",
"css-color-function": "^1.3.3",
"dsa-connect": "^0.4.2",
"dsa-connect": "^0.4.3",
"nuxt": "^2.15.7",
"qrcode": "^1.4.4",
"v-click-outside": "^3.1.2",

View File

@ -23,21 +23,177 @@
</div>
<h1 class="ml-4 text-primary-black text-2xl font-semibold">Liquity</h1>
</div>
<div class="mt-10">
<h2 class="text-primary-gray text-lg font-semibold">Overview</h2>
<div
class="px-1 mt-6 grid w-full grid-cols-1 gap-4 sm:grid-cols-3 xl:gap-[18px]"
>
<div class="shadow rounded-lg py-8 px-6 flex">
<div class="flex-1">
<h3 class="text-2xl text-primary-black font-medium">
{{ formatUsd(netValue) }}
</h3>
<p class="mt-4 text-primary-gray font-medium">Net Value</p>
</div>
<div class="flex items-center">
<SVGBalance />
</div>
</div>
<div class="shadow rounded-lg py-8 px-6 flex">
<div class="flex-1">
<h3 class="text-2xl text-primary-black font-medium">
{{ formatPercent(borrowFee) }}
</h3>
<p class="mt-4 text-primary-gray font-medium">Borrow Fee</p>
</div>
<div class="flex items-center">
<SVGPercent class="h-12" />
</div>
</div>
<div class="shadow rounded-lg py-8 px-6 flex">
<div class="flex-1">
<div class="flex justify-between items-center">
<h3 class="text-2xl text-primary-black font-medium">
{{ formatPercent(status) }}
</h3>
<Badge class="w-18 xxl:w-23" :color="color">{{ text }}</Badge>
</div>
<div
class="mt-4 flex justify-between items-center text-primary-gray font-medium"
>
<div class="flex items-center whitespace-no-wrap">
<div>D/C (%)</div>
<div class="ml-2"><Info text="Debt/Collateral ratio" /></div>
</div>
<span>Max - {{ formatPercent(liquidation) }}</span>
</div>
</div>
</div>
<div class="shadow rounded-lg py-8 px-6 flex">
<div class="flex-1">
<h3 class="text-2xl text-primary-black font-medium">
{{ formatUsdMax(liquidationPrice, liquidationMaxPrice) }} /
{{ formatUsd(liquidationMaxPrice) }}
</h3>
<p class="mt-4 text-primary-gray font-medium">Liquidation (ETH)</p>
</div>
<div class="flex items-center">
<IconBackground
name="receipt-tax"
class="bg-light-brown-pure text-light-brown-pure"
/>
</div>
</div>
</div>
</div>
<div class="mt-[60px]">
<div
class="w-full flex flex-col mt-6 sm:flex-row sm:items-center sm:justify-between xl:mt-4"
>
<h2 class="text-primary-gray text-lg font-semibold">Your Positions</h2>
</div>
<div
class="mt-3 grid w-full grid-cols-1 gap-4 sm:grid-cols-2 xxl:gap-6 min-w-max-content px-1"
>
<button-dashed
v-if="!troveOpened"
color="ocean-blue"
class="col-span-full"
height="80px"
full-width
@click="openNewTrove"
>
<SVGAdd class="w-3 mr-2" />
Open Trove
</button-dashed>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from "@nuxtjs/composition-api";
import { defineComponent, computed, useRouter } from "@nuxtjs/composition-api";
import BackIcon from "~/assets/icons/back.svg?inline";
import LiquityIcon from '~/assets/icons/liquity.svg?inline'
import SVGIncoming from "@/assets/img/icons/incoming.svg?inline";
import SVGBalance from "@/assets/img/icons/balance.svg?inline";
import SVGEarnings from "@/assets/img/icons/earnings.svg?inline";
import SVGArrowRight from "@/assets/img/icons/arrow-right.svg?inline";
import SVGPercent from "@/assets/img/icons/percent.svg?inline";
import SVGAdd from "~/assets/img/icons/add.svg?inline";
import LiquityIcon from "~/assets/icons/liquity.svg?inline";
import ButtonDashed from "~/components/common/input/ButtonDashed.vue";
import { useLiquityPosition } from "~/composables/protocols/useLiquityPosition";
import { useFormatting } from "~/composables/useFormatting";
import { useStatus } from "~/composables/useStatus";
import { useBigNumber } from "~/composables/useBigNumber";
export default defineComponent({
components: {
BackIcon,
LiquityIcon,
ButtonDashed,
SVGAdd,
SVGBalance,
SVGPercent,
},
setup() {
const router = useRouter();
const { div, isZero, gt, lt } = useBigNumber();
const {
formatUsd,
formatUsdMax,
formatPercent,
formatDecimal
} = useFormatting();
const {
troveOpened,
netValue,
borrowFee,
status,
liquidation,
liquidationPrice,
liquidationMaxPrice
} = useLiquityPosition();
const statusLiquidationRatio = computed(() =>
div(status.value, liquidation.value).toFixed()
);
const { color, text } = useStatus(statusLiquidationRatio);
function openNewTrove() {
router.push({ hash: "trove-new" });
}
return {
color,
text,
formatUsd,
formatUsdMax,
formatPercent,
formatDecimal,
troveOpened,
netValue,
borrowFee,
status,
liquidation,
liquidationPrice,
liquidationMaxPrice,
openNewTrove
};
}
});
</script>

View File

@ -4675,10 +4675,10 @@ drbg.js@^1.0.1:
create-hash "^1.1.2"
create-hmac "^1.1.4"
dsa-connect@^0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/dsa-connect/-/dsa-connect-0.4.2.tgz#1fb200eb3a93d260acada2cdbe8d5ebb71e3ba95"
integrity sha512-NW9kblkfEgoYx7uyz0TiWauIya09XMnyDQ/kRCleSRo9wAmK3gGLLuPXMAtkk8P2jvgMxIce9zeJvFvYcGdX8Q==
dsa-connect@^0.4.3:
version "0.4.3"
resolved "https://registry.yarnpkg.com/dsa-connect/-/dsa-connect-0.4.3.tgz#382ad7c1b1fa54f963c84adc353dea4bde2f6e80"
integrity sha512-kbG46cvAR2muy2P5jOTVsSZmyDewTAA0lKAeddsrVtpbmys1ujSuDjY/su4At6fAg/iF9/k+ltJAOEvoqt6g9A==
duplexer3@^0.1.4:
version "0.1.4"