Account health email alerts (#111)
* create alerts modal * add translation variables * translation variables * handle error states * add chinese * limit number of active alerts Co-authored-by: rjpeterson <r.james.peterson@gmail.com> Co-authored-by: Maximilian Schneider <mail@maximilianschneider.net>
This commit is contained in:
parent
63adaad55d
commit
320cd06d76
|
@ -12,10 +12,11 @@ import {
|
|||
ExternalLinkIcon,
|
||||
HeartIcon,
|
||||
} from '@heroicons/react/solid'
|
||||
import { BellIcon } from '@heroicons/react/outline'
|
||||
import useMangoStore, { MNGO_INDEX } from '../stores/useMangoStore'
|
||||
import { abbreviateAddress, formatUsdValue, usdFormatter } from '../utils'
|
||||
import { notify } from '../utils/notifications'
|
||||
import { LinkButton } from './Button'
|
||||
import { IconButton, LinkButton } from './Button'
|
||||
import { ElementTitle } from './styles'
|
||||
import Tooltip from './Tooltip'
|
||||
import DepositModal from './DepositModal'
|
||||
|
@ -27,6 +28,7 @@ import { breakpoints } from './TradePageGrid'
|
|||
import { useTranslation } from 'next-i18next'
|
||||
import useMangoAccount from '../hooks/useMangoAccount'
|
||||
import Loading from './Loading'
|
||||
import CreateAlertModal from './CreateAlertModal'
|
||||
|
||||
const I80F48_100 = I80F48.fromString('100')
|
||||
|
||||
|
@ -45,6 +47,7 @@ export default function AccountInfo() {
|
|||
|
||||
const [showDepositModal, setShowDepositModal] = useState(false)
|
||||
const [showWithdrawModal, setShowWithdrawModal] = useState(false)
|
||||
const [showAlertsModal, setShowAlertsModal] = useState(false)
|
||||
|
||||
const handleCloseDeposit = useCallback(() => {
|
||||
setShowDepositModal(false)
|
||||
|
@ -54,6 +57,10 @@ export default function AccountInfo() {
|
|||
setShowWithdrawModal(false)
|
||||
}, [])
|
||||
|
||||
const handleCloseAlerts = useCallback(() => {
|
||||
setShowAlertsModal(false)
|
||||
}, [])
|
||||
|
||||
const equity = mangoAccount
|
||||
? mangoAccount.computeValue(mangoGroup, mangoCache)
|
||||
: ZERO_I80F48
|
||||
|
@ -129,23 +136,33 @@ export default function AccountInfo() {
|
|||
id="account-details-tip"
|
||||
>
|
||||
{!isMobile ? (
|
||||
<ElementTitle>
|
||||
<Tooltip
|
||||
content={
|
||||
mangoAccount ? (
|
||||
<div>
|
||||
{t('init-health')}: {initHealth.toFixed(4)}
|
||||
<br />
|
||||
{t('maint-health')}: {maintHealth.toFixed(4)}
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('account')}
|
||||
</Tooltip>
|
||||
</ElementTitle>
|
||||
mangoAccount ? (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="h-8 w-8" />
|
||||
<ElementTitle>
|
||||
<Tooltip
|
||||
content={
|
||||
mangoAccount ? (
|
||||
<div>
|
||||
{t('init-health')}: {initHealth.toFixed(4)}
|
||||
<br />
|
||||
{t('maint-health')}: {maintHealth.toFixed(4)}
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('account')}
|
||||
</Tooltip>
|
||||
</ElementTitle>
|
||||
<IconButton onClick={() => setShowAlertsModal(true)}>
|
||||
<BellIcon className={`w-4 h-4`} />
|
||||
</IconButton>
|
||||
</div>
|
||||
) : (
|
||||
<ElementTitle>{t('account')}</ElementTitle>
|
||||
)
|
||||
) : null}
|
||||
<div>
|
||||
{mangoAccount ? (
|
||||
|
@ -360,6 +377,13 @@ export default function AccountInfo() {
|
|||
onClose={handleCloseWithdraw}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showAlertsModal && (
|
||||
<CreateAlertModal
|
||||
isOpen={showAlertsModal}
|
||||
onClose={handleCloseAlerts}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -87,7 +87,7 @@ const AccountsModal: FunctionComponent<AccountsModalProps> = ({
|
|||
>
|
||||
<div className="flex items-center">
|
||||
<PlusCircleIcon className="h-5 w-5 mr-1.5" />
|
||||
{t('new-account')}
|
||||
{t('new')}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,242 @@
|
|||
import React, { FunctionComponent, useEffect, useState } from 'react'
|
||||
import { PlusCircleIcon, TrashIcon } from '@heroicons/react/outline'
|
||||
import Modal from './Modal'
|
||||
import Input from './Input'
|
||||
import { ElementTitle } from './styles'
|
||||
import useMangoStore from '../stores/useMangoStore'
|
||||
import Button, { LinkButton } from './Button'
|
||||
import { notify } from '../utils/notifications'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import ButtonGroup from './ButtonGroup'
|
||||
import InlineNotification from './InlineNotification'
|
||||
|
||||
interface CreateAlertModalProps {
|
||||
onClose: () => void
|
||||
isOpen: boolean
|
||||
repayAmount?: string
|
||||
tokenSymbol?: string
|
||||
}
|
||||
|
||||
const CreateAlertModal: FunctionComponent<CreateAlertModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
}) => {
|
||||
const { t } = useTranslation('common')
|
||||
const actions = useMangoStore((s) => s.actions)
|
||||
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
|
||||
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
|
||||
const activeAlerts = useMangoStore((s) => s.alerts.activeAlerts)
|
||||
const loading = useMangoStore((s) => s.alerts.loading)
|
||||
const submitting = useMangoStore((s) => s.alerts.submitting)
|
||||
const error = useMangoStore((s) => s.alerts.error)
|
||||
const [email, setEmail] = useState<string>('')
|
||||
const [invalidAmountMessage, setInvalidAmountMessage] = useState('')
|
||||
const [health, setHealth] = useState('')
|
||||
const [showCustomHealthForm, setShowCustomHealthForm] = useState(false)
|
||||
const [showAlertForm, setShowAlertForm] = useState(false)
|
||||
|
||||
const healthPresets = ['5', '10', '15', '25', '30']
|
||||
|
||||
const validateEmailInput = (amount) => {
|
||||
if (Number(amount) <= 0) {
|
||||
setInvalidAmountMessage(t('enter-amount'))
|
||||
}
|
||||
}
|
||||
|
||||
const onChangeEmailInput = (amount) => {
|
||||
setEmail(amount)
|
||||
setInvalidAmountMessage('')
|
||||
}
|
||||
|
||||
async function onCreateAlert() {
|
||||
if (!email) {
|
||||
notify({
|
||||
title: 'An email address is required',
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
} else if (!health) {
|
||||
notify({
|
||||
title: 'Alert health is required',
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
const body = {
|
||||
mangoGroupPk: mangoGroup.publicKey.toString(),
|
||||
mangoAccountPk: mangoAccount.publicKey.toString(),
|
||||
health,
|
||||
alertProvider: 'mail',
|
||||
email,
|
||||
}
|
||||
const success: any = await actions.createAlert(body)
|
||||
if (success) {
|
||||
setShowAlertForm(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancelCreateAlert = () => {
|
||||
if (activeAlerts.length > 0) {
|
||||
setShowAlertForm(false)
|
||||
} else {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
actions.loadAlerts(mangoAccount.publicKey)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
{!loading && !submitting ? (
|
||||
<>
|
||||
{activeAlerts.length > 0 && !showAlertForm ? (
|
||||
<>
|
||||
<Modal.Header>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="w-20" />
|
||||
<ElementTitle noMarignBottom>
|
||||
{t('active-alerts')}
|
||||
</ElementTitle>
|
||||
<Button
|
||||
className="col-span-1 flex items-center justify-center pt-0 pb-0 h-8 text-xs w-20"
|
||||
disabled={activeAlerts.length >= 3}
|
||||
onClick={() => setShowAlertForm(true)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<PlusCircleIcon className="h-4 w-4 mr-1.5" />
|
||||
{t('new')}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</Modal.Header>
|
||||
<div className="border-b border-th-fgd-4">
|
||||
{activeAlerts.map((alert, index) => (
|
||||
<div
|
||||
className="border-t border-th-fgd-4 flex items-center justify-between p-4"
|
||||
key={`${alert._id}${index}`}
|
||||
>
|
||||
<div className="text-th-fgd-1">
|
||||
{t('alert-info', { health: alert.health })}
|
||||
</div>
|
||||
<TrashIcon
|
||||
className="cursor-pointer default-transition h-5 text-th-fgd-3 w-5 hover:text-th-primary"
|
||||
onClick={() => actions.deleteAlert(alert._id)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{activeAlerts.length >= 3 ? (
|
||||
<div className="mt-1 text-center text-xxs text-th-fgd-3">
|
||||
{t('alerts-max')}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : showAlertForm ? (
|
||||
<>
|
||||
<div>
|
||||
<ElementTitle noMarignBottom>{t('create-alert')}</ElementTitle>
|
||||
<p className="mt-1 text-center text-th-fgd-4">
|
||||
{t('alerts-disclaimer')}
|
||||
</p>
|
||||
</div>
|
||||
{error ? (
|
||||
<div className="my-4">
|
||||
<InlineNotification title={error} type="error" />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mb-1.5 text-th-fgd-1">{t('email-address')}</div>
|
||||
<Input
|
||||
type="email"
|
||||
className={`border border-th-fgd-4 flex-grow pr-11`}
|
||||
error={!!invalidAmountMessage}
|
||||
onBlur={(e) => validateEmailInput(e.target.value)}
|
||||
value={email || ''}
|
||||
onChange={(e) => onChangeEmailInput(e.target.value)}
|
||||
/>
|
||||
<div className="flex items-end mt-4">
|
||||
<div className="w-full">
|
||||
<div className="flex justify-between mb-1.5">
|
||||
<div className="text-th-fgd-1">{t('alert-health')}</div>
|
||||
<LinkButton
|
||||
className="font-normal text-th-fgd-3 text-xs"
|
||||
onClick={() =>
|
||||
setShowCustomHealthForm(!showCustomHealthForm)
|
||||
}
|
||||
>
|
||||
{showCustomHealthForm ? t('presets') : t('custom')}
|
||||
</LinkButton>
|
||||
</div>
|
||||
{showCustomHealthForm ? (
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
onChange={(e) => setHealth(e.target.value)}
|
||||
suffix={
|
||||
<div className="font-bold text-base text-th-fgd-3">
|
||||
%
|
||||
</div>
|
||||
}
|
||||
value={health}
|
||||
/>
|
||||
) : (
|
||||
<ButtonGroup
|
||||
activeValue={health.toString()}
|
||||
onChange={(p) => setHealth(p)}
|
||||
unit="%"
|
||||
values={healthPresets}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center mt-6">
|
||||
<Button onClick={() => onCreateAlert()}>
|
||||
{t('create-alert')}
|
||||
</Button>
|
||||
<LinkButton
|
||||
className="ml-4 text-th-fgd-3 hover:text-th-fgd-1"
|
||||
onClick={handleCancelCreateAlert}
|
||||
>
|
||||
{t('cancel')}
|
||||
</LinkButton>
|
||||
</div>
|
||||
</>
|
||||
) : error ? (
|
||||
<div>
|
||||
<InlineNotification title={error} type="error" />
|
||||
<Button
|
||||
className="flex justify-center mt-6 mx-auto"
|
||||
onClick={() => actions.loadAlerts()}
|
||||
>
|
||||
{t('try-again')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Modal.Header>
|
||||
<ElementTitle noMarignBottom>{t('no-alerts')}</ElementTitle>
|
||||
</Modal.Header>
|
||||
<p className="mb-4 text-center">{t('no-alerts-desc')}</p>
|
||||
<Button
|
||||
className="flex justify-center m-auto"
|
||||
onClick={() => setShowAlertForm(true)}
|
||||
>
|
||||
{t('new-alert')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className="animate-pulse bg-th-bkg-3 h-12 rounded-md w-full" />
|
||||
<div className="animate-pulse bg-th-bkg-3 h-12 rounded-md w-full" />
|
||||
<div className="animate-pulse bg-th-bkg-3 h-12 rounded-md w-full" />
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(CreateAlertModal)
|
|
@ -453,7 +453,9 @@ const JupiterForm: FunctionComponent = () => {
|
|||
className={`bg-th-bkg-3 max-h-[500px] overflow-auto pb-4 pt-6 rounded-r-md w-64 thin-scroll`}
|
||||
>
|
||||
<div className="flex items-center justify-between pb-2 px-4">
|
||||
<div className="font-bold text-base text-th-fgd-1">{t('wallet')}</div>
|
||||
<div className="font-bold text-base text-th-fgd-1">
|
||||
{t('wallet')}
|
||||
</div>
|
||||
<a
|
||||
className="flex items-center text-th-fgd-4 text-xs hover:text-th-fgd-3"
|
||||
href={`https://explorer.solana.com/address/${wallet?.publicKey}`}
|
||||
|
@ -639,7 +641,7 @@ const JupiterForm: FunctionComponent = () => {
|
|||
<div>
|
||||
<div className="text-th-fgd-4 text-center text-xs">
|
||||
{t('swap:routes-found', {
|
||||
numberOfRoutes: routes?.length
|
||||
numberOfRoutes: routes?.length,
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
|
@ -830,7 +832,11 @@ const JupiterForm: FunctionComponent = () => {
|
|||
}}
|
||||
className="h-12 mt-6 text-base w-full"
|
||||
>
|
||||
{connected ? (swapping ? t('swap:swapping') : t('swap')) : t('connect-wallet')}
|
||||
{connected
|
||||
? swapping
|
||||
? t('swap:swapping')
|
||||
: t('swap')
|
||||
: t('connect-wallet')}
|
||||
</Button>
|
||||
{inputTokenStats?.prices?.length && outputTokenStats?.prices?.length ? (
|
||||
<>
|
||||
|
@ -982,7 +988,7 @@ const JupiterForm: FunctionComponent = () => {
|
|||
<div className="flex justify-between" key={index}>
|
||||
<span>
|
||||
{t('swap:fees-paid-to', {
|
||||
feeRecipient: info.marketMeta?.amm?.label
|
||||
feeRecipient: info.marketMeta?.amm?.label,
|
||||
})}
|
||||
</span>
|
||||
<span className="text-th-fgd-1">
|
||||
|
@ -1011,15 +1017,16 @@ const JupiterForm: FunctionComponent = () => {
|
|||
<span className="text-th-fgd-1">
|
||||
{t('swap:ata-deposit-details', {
|
||||
cost: depositAndFee?.ataDeposit / Math.pow(10, 9),
|
||||
count: depositAndFee?.ataDepositLength
|
||||
count: depositAndFee?.ataDepositLength,
|
||||
})}
|
||||
</span>
|
||||
{depositAndFee?.openOrdersDeposits?.length ? (
|
||||
<span className="text-th-fgd-1">
|
||||
{t('swap:serum-details', {
|
||||
cost: sum(depositAndFee?.openOrdersDeposits) /
|
||||
Math.pow(10, 9),
|
||||
count: depositAndFee?.openOrdersDeposits.length
|
||||
cost:
|
||||
sum(depositAndFee?.openOrdersDeposits) /
|
||||
Math.pow(10, 9),
|
||||
count: depositAndFee?.openOrdersDeposits.length,
|
||||
})}
|
||||
</span>
|
||||
) : null}
|
||||
|
@ -1124,7 +1131,7 @@ const JupiterForm: FunctionComponent = () => {
|
|||
<div key={index}>
|
||||
<span>
|
||||
{t('swap:fees-paid-to', {
|
||||
feeRecipient: info.marketMeta?.amm?.label
|
||||
feeRecipient: info.marketMeta?.amm?.label,
|
||||
})}
|
||||
</span>
|
||||
<div className="text-th-fgd-1">
|
||||
|
@ -1156,9 +1163,9 @@ const JupiterForm: FunctionComponent = () => {
|
|||
return (
|
||||
<div key={index}>
|
||||
<span>
|
||||
{t('swap:fees-paid-to', {
|
||||
feeRecipient: info.marketMeta?.amm?.label
|
||||
})}
|
||||
{t('swap:fees-paid-to', {
|
||||
feeRecipient: info.marketMeta?.amm?.label,
|
||||
})}
|
||||
</span>
|
||||
<div className="mt-0.5 text-th-fgd-1">
|
||||
{(
|
||||
|
@ -1190,9 +1197,7 @@ const JupiterForm: FunctionComponent = () => {
|
|||
content={
|
||||
<>
|
||||
{depositAndFee?.ataDepositLength ? (
|
||||
<div>
|
||||
{t('swap:need-ata-account')}
|
||||
</div>
|
||||
<div>{t('swap:need-ata-account')}</div>
|
||||
) : null}
|
||||
{depositAndFee?.openOrdersDeposits?.length ? (
|
||||
<div className="mt-2">
|
||||
|
@ -1217,16 +1222,21 @@ const JupiterForm: FunctionComponent = () => {
|
|||
{depositAndFee?.ataDepositLength ? (
|
||||
<div className="mt-0.5 text-th-fgd-1">
|
||||
{t('swap:ata-deposit-details', {
|
||||
cost: (depositAndFee?.ataDeposit / Math.pow(10, 9)).toFixed(5),
|
||||
count: depositAndFee?.ataDepositLength
|
||||
cost: (
|
||||
depositAndFee?.ataDeposit / Math.pow(10, 9)
|
||||
).toFixed(5),
|
||||
count: depositAndFee?.ataDepositLength,
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{depositAndFee?.openOrdersDeposits?.length ? (
|
||||
<div className="mt-0.5 text-th-fgd-1">
|
||||
{t('swap:serum-details', {
|
||||
cost: (sum(depositAndFee?.openOrdersDeposits) / Math.pow(10, 9)).toFixed(5),
|
||||
count: depositAndFee?.openOrdersDeposits.length
|
||||
cost: (
|
||||
sum(depositAndFee?.openOrdersDeposits) /
|
||||
Math.pow(10, 9)
|
||||
).toFixed(5),
|
||||
count: depositAndFee?.openOrdersDeposits.length,
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
|
|
|
@ -13,8 +13,13 @@
|
|||
"account-risk": "Account Risk",
|
||||
"account-value": "Account Value",
|
||||
"accounts": "Accounts",
|
||||
"active-alerts": "Active Alerts",
|
||||
"add-more-sol": "Add more SOL to your wallet to avoid failed transactions.",
|
||||
"add-name": "Add Name",
|
||||
"alert-health": "Alert when health is below",
|
||||
"alert-info": "Email when health <= {{health}}%",
|
||||
"alerts-disclaimer": "Do not rely solely on alerts to protect your account. We can't guarantee they will be delivered.",
|
||||
"alerts-max": "You've reached the maximum number of active alerts.",
|
||||
"all-assets": "All Assets",
|
||||
"amount": "Amount",
|
||||
"approximate-time": "Approx Time",
|
||||
|
@ -77,6 +82,7 @@
|
|||
"country-not-allowed": "Country Not Allowed",
|
||||
"country-not-allowed-tooltip": "You are using an open-source frontend facilitated by the Mango DAO. As such, it restricts access to certain regions out of an abundance of caution, due to regulatory uncertainty.",
|
||||
"create-account": "Create Account",
|
||||
"create-alert": "Create Alert",
|
||||
"current-stats": "Current Stats",
|
||||
"custom": "Custom",
|
||||
"daily-change": "Daily Change",
|
||||
|
@ -108,6 +114,7 @@
|
|||
"edit": "Edit",
|
||||
"edit-name": "Edit Name",
|
||||
"edit-nickname": "Edit the public nickname for your account",
|
||||
"email-address": "Email Address",
|
||||
"english": "English",
|
||||
"enter-amount": "Enter an amount to deposit",
|
||||
"enter-name": "Enter an account name",
|
||||
|
@ -206,12 +213,16 @@
|
|||
"name-your-account": "Name Your Account",
|
||||
"net": "Net",
|
||||
"net-balance": "Net Balance",
|
||||
"new": "New",
|
||||
"new-alert": "New Alert",
|
||||
"net-interest-value": "Net Interest Value",
|
||||
"net-interest-value-desc": "Calculated at the time it was earned/paid. This might be useful at tax time.",
|
||||
"new-account": "New",
|
||||
"next": "Next",
|
||||
"no-account-found": "No Account Found",
|
||||
"no-address": "No {{tokenSymbol}} wallet address found",
|
||||
"no-alerts": "No Active Alerts",
|
||||
"no-alerts-desc": "Create an alert to be notified when your account health is low.",
|
||||
"no-balances": "No balances",
|
||||
"no-borrows": "No borrows found.",
|
||||
"no-funding": "No funding earned or paid",
|
||||
|
@ -355,7 +366,7 @@
|
|||
"trades-history": "Trade History",
|
||||
"transaction-sent": "Transaction sent",
|
||||
"trigger-price": "Trigger Price",
|
||||
"try-again": "Please try again",
|
||||
"try-again": "Try again",
|
||||
"type": "Type",
|
||||
"unrealized-pnl": "Unrealized PnL",
|
||||
"unsettled": "Unsettled",
|
||||
|
|
|
@ -13,8 +13,13 @@
|
|||
"account-risk": "Riesgo de cuenta",
|
||||
"account-value": "Valor de la cuenta",
|
||||
"accounts": "Cuentas",
|
||||
"active-alerts": "Active Alerts",
|
||||
"add-more-sol": "Add more SOL to your wallet to avoid failed transactions.",
|
||||
"add-name": "Añadir nombre",
|
||||
"alert-health": "Alert when health is below",
|
||||
"alert-info": "Email when health <= {{health}}%",
|
||||
"alerts-disclaimer": "Do not rely solely on alerts to protect your account. We can't guarantee they will be delivered.",
|
||||
"alerts-max": "You've reached the maximum number of active alerts.",
|
||||
"all-assets": "Todos los activos",
|
||||
"amount": "Monto",
|
||||
"approximate-time": "Tiempo aproximado",
|
||||
|
@ -76,6 +81,7 @@
|
|||
"copy-address": "Copy address",
|
||||
"country-not-allowed": "País no permitido",
|
||||
"create-account": "Create Account",
|
||||
"create-alert": "Create Alert",
|
||||
"current-stats": "Estadísticas actuales",
|
||||
"custom": "Personalizada",
|
||||
"daily-change": "Cambio diario",
|
||||
|
@ -107,6 +113,7 @@
|
|||
"edit": "Editar",
|
||||
"edit-name": "Edit Name",
|
||||
"edit-nickname": "Edite el apodo público de su cuenta",
|
||||
"email-address": "Email Address",
|
||||
"english": "English",
|
||||
"enter-amount": "Ingrese una cantidad para depositar",
|
||||
"enter-name": "Ingrese un nombre de cuenta",
|
||||
|
@ -204,12 +211,16 @@
|
|||
"name-your-account": "Nombra tu cuenta",
|
||||
"net": "Neto",
|
||||
"net-balance": "Balance neto",
|
||||
"new": "Nuevo",
|
||||
"new-alert": "New Alert",
|
||||
"net-interest-value": "Valor de interés neto",
|
||||
"net-interest-value-desc": "Calculado en el momento en que se ganó / pagó. Esto podría ser útil al momento de impuestos.",
|
||||
"new-account": "Nuevo",
|
||||
"next": "Próximo",
|
||||
"no-account-found": "No Account Found",
|
||||
"no-address": "No ${tokenSymbol} dirección de billetera encontrada",
|
||||
"no-alerts": "No Active Alerts",
|
||||
"no-alerts-desc": "Create an alert to be notified when your account health is low.",
|
||||
"no-balances": "Sin saldos",
|
||||
"no-borrows": "No se encontraron préstamos.",
|
||||
"no-funding": "Sin fondos ganados / pagados",
|
||||
|
|
|
@ -13,8 +13,13 @@
|
|||
"account-risk": "帐户风险度",
|
||||
"account-value": "帐户价值",
|
||||
"accounts": "帐户",
|
||||
"active-alerts": "活动警报",
|
||||
"add-more-sol": "为了避免交易出错请给被连结的钱包多存入一点SOL。",
|
||||
"add-name": "加标签",
|
||||
"alert-health": "Alert when health is below",
|
||||
"alert-info": "健康度在{{health}}%以下时发电子邮件",
|
||||
"alerts-disclaimer": "请别全靠警报来保护资产。我们无法保证会准时发出。",
|
||||
"alerts-max": "You've reached the maximum number of active alerts.",
|
||||
"all-assets": "所有资产",
|
||||
"amount": "数量",
|
||||
"approximate-time": "大概时间",
|
||||
|
@ -76,6 +81,7 @@
|
|||
"copy-address": "复制地址",
|
||||
"country-not-allowed": "您的国家不允许",
|
||||
"create-account": "创建帐户",
|
||||
"create-alert": "创建警报",
|
||||
"current-stats": "当前统计",
|
||||
"custom": "自定义",
|
||||
"daily-change": "24小时变动",
|
||||
|
@ -107,6 +113,7 @@
|
|||
"edit": "编辑",
|
||||
"edit-name": "编辑帐户标签",
|
||||
"edit-nickname": "编辑帐户标签",
|
||||
"email-address": "电子邮件地址",
|
||||
"english": "English",
|
||||
"enter-amount": "输入存款数量",
|
||||
"enter-name": "输入帐户标签",
|
||||
|
@ -204,12 +211,16 @@
|
|||
"name-your-account": "给帐户标签",
|
||||
"net": "净",
|
||||
"net-balance": "净余额",
|
||||
"new": "新子帐户",
|
||||
"new-alert": "创建警报",
|
||||
"net-interest-value": "Net Interest Value",
|
||||
"net-interest-value-desc": "Calculated at the time it was earned/paid. This might be useful at tax time.",
|
||||
"new-account": "新子帐户",
|
||||
"next": "前往",
|
||||
"no-account-found": "您没有帐户",
|
||||
"no-address": "没有{{tokenSymbol}}钱包地址",
|
||||
"no-alerts": "您没有活动警报",
|
||||
"no-alerts-desc": "创建警报而健康度低时被通知到。",
|
||||
"no-balances": "您没有余额",
|
||||
"no-borrows": "您没有借贷。",
|
||||
"no-funding": "您未收/付过资金费",
|
||||
|
|
|
@ -13,8 +13,13 @@
|
|||
"account-risk": "帳戶風險度",
|
||||
"account-value": "帳戶價值",
|
||||
"accounts": "帳戶",
|
||||
"active-alerts": "活動警報",
|
||||
"add-more-sol": "為了避免交易出錯請給被連結的錢包多存入一點SOL。",
|
||||
"add-name": "加標籤",
|
||||
"alert-health": "Alert when health is below",
|
||||
"alert-info": "健康度在{{health}}%以下時發電子郵件",
|
||||
"alerts-disclaimer": "請別全靠警報來保護資產。我們無法保證會準時發出。",
|
||||
"alerts-max": "You've reached the maximum number of active alerts.",
|
||||
"all-assets": "所有資產",
|
||||
"amount": "數量",
|
||||
"approximate-time": "大概時間",
|
||||
|
@ -76,6 +81,7 @@
|
|||
"copy-address": "複製地址",
|
||||
"country-not-allowed": "您的國家不允許",
|
||||
"create-account": "創建帳戶",
|
||||
"create-alert": "創建警報",
|
||||
"current-stats": "當前統計",
|
||||
"custom": "自定義",
|
||||
"daily-change": "24小時變動",
|
||||
|
@ -107,6 +113,7 @@
|
|||
"edit": "編輯",
|
||||
"edit-name": "編輯帳戶標籤",
|
||||
"edit-nickname": "編輯帳戶標籤",
|
||||
"email-address": "電子郵件地址",
|
||||
"english": "English",
|
||||
"enter-amount": "輸入存款數量",
|
||||
"enter-name": "輸入帳戶標籤",
|
||||
|
@ -204,12 +211,16 @@
|
|||
"name-your-account": "給帳戶標籤",
|
||||
"net": "淨",
|
||||
"net-balance": "淨餘額",
|
||||
"new": "新子帳戶",
|
||||
"new-alert": "創建警報",
|
||||
"net-interest-value": "Net Interest Value",
|
||||
"net-interest-value-desc": "Calculated at the time it was earned/paid. This might be useful at tax time.",
|
||||
"new-account": "新子帳戶",
|
||||
"next": "前往",
|
||||
"no-account-found": "您沒有帳戶",
|
||||
"no-address": "沒有{{tokenSymbol}}錢包地址",
|
||||
"no-alerts": "您沒有活動警報",
|
||||
"no-alerts-desc": "創建警報而健康度低時被通知到。",
|
||||
"no-balances": "您沒有餘額",
|
||||
"no-borrows": "您沒有借貸。",
|
||||
"no-funding": "您未收/付過資金費",
|
||||
|
|
|
@ -104,6 +104,24 @@ export interface Orderbook {
|
|||
asks: number[][]
|
||||
}
|
||||
|
||||
export interface Alert {
|
||||
acc: PublicKey
|
||||
alertProvider: 'mail'
|
||||
health: number
|
||||
_id: string
|
||||
open: boolean
|
||||
timestamp: number
|
||||
triggeredTimestamp: number | undefined
|
||||
}
|
||||
|
||||
interface AlertRequest {
|
||||
alertProvider: 'mail'
|
||||
health: number
|
||||
mangoGroupPk: string
|
||||
mangoAccountPk: string
|
||||
email: string | undefined
|
||||
}
|
||||
|
||||
interface MangoStore extends State {
|
||||
notificationIdCounter: number
|
||||
notifications: Array<Notification>
|
||||
|
@ -171,6 +189,14 @@ interface MangoStore extends State {
|
|||
actions: {
|
||||
[key: string]: (args?) => void
|
||||
}
|
||||
alerts: {
|
||||
activeAlerts: Array<Alert>
|
||||
triggeredAlerts: Array<Alert>
|
||||
loading: boolean
|
||||
error: string
|
||||
submitting: boolean
|
||||
success: string
|
||||
}
|
||||
}
|
||||
|
||||
const useMangoStore = create<MangoStore>((set, get) => {
|
||||
|
@ -248,6 +274,14 @@ const useMangoStore = create<MangoStore>((set, get) => {
|
|||
settings: {
|
||||
uiLocked: true,
|
||||
},
|
||||
alerts: {
|
||||
activeAlerts: [],
|
||||
triggeredAlerts: [],
|
||||
loading: false,
|
||||
error: '',
|
||||
submitting: false,
|
||||
success: '',
|
||||
},
|
||||
tradeHistory: [],
|
||||
set: (fn) => set(produce(fn)),
|
||||
actions: {
|
||||
|
@ -558,6 +592,160 @@ const useMangoStore = create<MangoStore>((set, get) => {
|
|||
state.connection.client = newClient
|
||||
})
|
||||
},
|
||||
async createAlert(req: AlertRequest) {
|
||||
const set = get().set
|
||||
const alert = {
|
||||
acc: new PublicKey(req.mangoAccountPk),
|
||||
alertProvider: req.alertProvider,
|
||||
health: req.health,
|
||||
open: true,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
|
||||
set((state) => {
|
||||
state.alerts.submitting = true
|
||||
state.alerts.error = ''
|
||||
state.alerts.success = ''
|
||||
})
|
||||
|
||||
const mangoAccount = get().selectedMangoAccount.current
|
||||
const mangoGroup = get().selectedMangoGroup.current
|
||||
const mangoCache = get().selectedMangoGroup.cache
|
||||
const currentAccHealth = await mangoAccount.getHealthRatio(
|
||||
mangoGroup,
|
||||
mangoCache,
|
||||
'Maint'
|
||||
)
|
||||
|
||||
if (currentAccHealth && currentAccHealth.toNumber() <= req.health) {
|
||||
set((state) => {
|
||||
state.alerts.submitting = false
|
||||
state.alerts.error = `Current account health is already below ${req.health}%`
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
const fetchUrl = `https://mango-alerts-v3.herokuapp.com/alerts`
|
||||
const headers = { 'Content-Type': 'application/json' }
|
||||
|
||||
const response = await fetch(fetchUrl, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify(req),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const alerts = get().alerts.activeAlerts
|
||||
|
||||
set((state) => {
|
||||
state.alerts.activeAlerts = [alert as Alert].concat(alerts)
|
||||
state.alerts.submitting = false
|
||||
state.alerts.success = 'Alert saved'
|
||||
})
|
||||
notify({
|
||||
title: 'Alert saved',
|
||||
type: 'success',
|
||||
})
|
||||
return true
|
||||
} else {
|
||||
set((state) => {
|
||||
state.alerts.error = 'Something went wrong'
|
||||
state.alerts.submitting = false
|
||||
})
|
||||
notify({
|
||||
title: 'Something went wrong',
|
||||
type: 'error',
|
||||
})
|
||||
return false
|
||||
}
|
||||
},
|
||||
async deleteAlert(id: string) {
|
||||
const set = get().set
|
||||
|
||||
set((state) => {
|
||||
state.alerts.submitting = true
|
||||
state.alerts.error = ''
|
||||
state.alerts.success = ''
|
||||
})
|
||||
|
||||
const fetchUrl = `https://mango-alerts-v3.herokuapp.com/delete-alert`
|
||||
const headers = { 'Content-Type': 'application/json' }
|
||||
|
||||
const response = await fetch(fetchUrl, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify({ id }),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const alerts = get().alerts.activeAlerts
|
||||
set((state) => {
|
||||
state.alerts.activeAlerts = alerts.filter(
|
||||
(alert) => alert._id !== id
|
||||
)
|
||||
state.alerts.submitting = false
|
||||
state.alerts.success = 'Alert deleted'
|
||||
})
|
||||
notify({
|
||||
title: 'Alert deleted',
|
||||
type: 'success',
|
||||
})
|
||||
} else {
|
||||
set((state) => {
|
||||
state.alerts.error = 'Something went wrong'
|
||||
state.alerts.submitting = false
|
||||
})
|
||||
notify({
|
||||
title: 'Something went wrong',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
},
|
||||
async loadAlerts(mangoAccountPk: PublicKey) {
|
||||
const set = get().set
|
||||
|
||||
set((state) => {
|
||||
state.alerts.error = ''
|
||||
state.alerts.loading = true
|
||||
})
|
||||
|
||||
const headers = { 'Content-Type': 'application/json' }
|
||||
const response = await fetch(
|
||||
`https://mango-alerts-v3.herokuapp.com/alerts/${mangoAccountPk}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
}
|
||||
)
|
||||
|
||||
if (response.ok) {
|
||||
const parsedResponse = await response.json()
|
||||
// sort active by latest creation time first
|
||||
const activeAlerts = parsedResponse.alerts
|
||||
.filter((alert) => alert.open)
|
||||
.sort((a, b) => {
|
||||
return b.timestamp - a.timestamp
|
||||
})
|
||||
|
||||
// sort triggered by latest trigger time first
|
||||
const triggeredAlerts = parsedResponse.alerts
|
||||
.filter((alert) => !alert.open)
|
||||
.sort((a, b) => {
|
||||
return b.triggeredTimestamp - a.triggeredTimestamp
|
||||
})
|
||||
|
||||
set((state) => {
|
||||
state.alerts.activeAlerts = activeAlerts
|
||||
state.alerts.triggeredAlerts = triggeredAlerts
|
||||
state.alerts.loading = false
|
||||
})
|
||||
} else {
|
||||
set((state) => {
|
||||
state.alerts.error = 'Error loading alerts'
|
||||
state.alerts.loading = false
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue