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:
saml33 2022-01-03 15:42:59 +11:00 committed by GitHub
parent 63adaad55d
commit 320cd06d76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 547 additions and 39 deletions

View File

@ -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}
/>
)}
</>
)
}

View File

@ -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>

View File

@ -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)

View File

@ -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}

View File

@ -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",

View File

@ -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",

View File

@ -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": "您未收/付过资金费",

View File

@ -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": "您未收/付過資金費",

View File

@ -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
})
}
},
},
}
})