diff --git a/app/positions/components/PositionsContent.tsx b/app/positions/components/PositionsContent.tsx index 8ef970da..14919508 100644 --- a/app/positions/components/PositionsContent.tsx +++ b/app/positions/components/PositionsContent.tsx @@ -36,7 +36,7 @@ export default function Positions() { const { account } = useParams<{ account: string }>(); const { address, isConnected } = useAccount(); - const { rebalancerInfo } = useUserRebalancerInfo(address); + const { rebalancerInfo, refetch: refetchRebalancerInfo } = useUserRebalancerInfo(address); const isOwner = useMemo(() => { if (!account) return false; @@ -157,7 +157,10 @@ export default function Positions() { setShowSetupAgentModal(false)} + onClose={() => { + void refetchRebalancerInfo(); + setShowSetupAgentModal(false); + }} account={account as Address} userRebalancerInfo={rebalancerInfo} /> diff --git a/app/positions/components/agent/Main.tsx b/app/positions/components/agent/Main.tsx new file mode 100644 index 00000000..260511d3 --- /dev/null +++ b/app/positions/components/agent/Main.tsx @@ -0,0 +1,180 @@ +import { Tooltip } from '@nextui-org/react'; +import { motion } from 'framer-motion'; +import moment from 'moment'; +import Image from 'next/image'; +import Link from 'next/link'; +import { GrStatusGood } from 'react-icons/gr'; +import { Button } from '@/components/common'; +import { TooltipContent } from '@/components/TooltipContent'; +import { useMarkets } from '@/contexts/MarketsContext'; +import { findAgent } from '@/utils/monarch-agent'; +import { findToken } from '@/utils/tokens'; +import { UserRebalancerInfo, UserTransaction } from '@/utils/types'; + +const img = require('../../../../src/imgs/agent/agent-detailed.png') as string; + +type MainProps = { + account?: string; + onNext: () => void; + userRebalancerInfo: UserRebalancerInfo; + history: UserTransaction[]; +}; + +export function Main({ account, onNext, userRebalancerInfo, history }: MainProps) { + const agent = findAgent(userRebalancerInfo.rebalancer); + const { markets } = useMarkets(); + + if (!agent) { + return null; + } + + // Find markets that the agent is authorized to manage + const authorizedMarkets = markets.filter((market) => + userRebalancerInfo.marketCaps.some( + (cap) => cap.marketId.toLowerCase() === market.uniqueKey.toLowerCase(), + ), + ); + + // Group markets by loan asset address + const loanAssetGroups = authorizedMarkets.reduce( + (acc, market) => { + const address = market.loanAsset.address.toLowerCase(); + if (!acc[address]) { + acc[address] = { + address, + chainId: market.morphoBlue.chain.id, + markets: [], + }; + } + acc[address].markets.push(market); + return acc; + }, + {} as Record, + ); + + // sort from newest to oldest + const lastTx = history + .sort((a, b) => b.timestamp - a.timestamp) + .find((tx) => + userRebalancerInfo.transactions.find( + (t) => t.transactionHash.toLowerCase() === tx.hash.toLowerCase(), + ), + ); + + return ( +
+ + Monarch Agent + + +
+
+
+

{agent.name}

+ + {agent.address.slice(0, 6) + '...' + agent.address.slice(-4)} + +
+ } + title="Agent Active" + detail="Your agent is actively managing your positions" + /> + } + > +
+
+ Active +
+ +
+ +
+
+

Strategy

+

{agent.strategyDescription}

+
+ +
+

Monitoring Positions

+
+ {Object.values(loanAssetGroups).map( + ({ address, chainId, markets: marketsForLoanAsset }) => { + const token = findToken(address, chainId); + return ( +
+ {token?.img && ( + {token.symbol} + )} + + {token?.symbol ?? 'Unknown'} ({marketsForLoanAsset.length}) + +
+ ); + }, + )} +
+
+ +
+

Last Action

+
+ {lastTx && ( + <> + + + {' '} + {lastTx.hash.slice(0, 6) + '...' + lastTx.hash.slice(-4)}{' '} + + {' '} + {moment.unix(lastTx.timestamp).fromNow()}{' '} + {' '} + + + + View All ({userRebalancerInfo.transactions?.length ?? 0}) + + + )} +
+
+
+
+ + +
+ ); +} diff --git a/app/positions/components/agent/SetupAgent.tsx b/app/positions/components/agent/SetupAgent.tsx index d5931aaa..48f0da1f 100644 --- a/app/positions/components/agent/SetupAgent.tsx +++ b/app/positions/components/agent/SetupAgent.tsx @@ -8,6 +8,7 @@ import { AgentSetupProcessModal } from '@/components/AgentSetupProcessModal'; import { Button } from '@/components/common/Button'; import { MarketInfoBlockCompact } from '@/components/common/MarketInfoBlock'; import { MarketCap, useAuthorizeAgent } from '@/hooks/useAuthorizeAgent'; +import { findAgent, KnownAgents } from '@/utils/monarch-agent'; import { SupportedNetworks } from '@/utils/networks'; import { OracleVendorIcons, OracleVendors } from '@/utils/oracle'; import { findToken } from '@/utils/tokens'; @@ -171,7 +172,17 @@ export function SetupAgent({ ); }; - const { executeBatchSetupAgent, currentStep } = useAuthorizeAgent(pendingCaps, onNext); + const hasSetupAgent = + userRebalancerInfo?.rebalancer.toLowerCase() === KnownAgents.MAX_APY.toLowerCase(); + + // todo: search user agent after + const agent = findAgent(KnownAgents.MAX_APY ?? ''); + + const { executeBatchSetupAgent, currentStep } = useAuthorizeAgent( + KnownAgents.MAX_APY, + pendingCaps, + onNext, + ); const handleExecute = useCallback(() => { setShowProcessModal(true); @@ -180,8 +191,14 @@ export function SetupAgent({ return (
+ {!hasSetupAgent && agent && ( +
+

{agent.name}

+

{agent.strategyDescription}

+
+ )}
- Monarch Agent can only reallocate funds among markets you authorize it to! + The agent can only reallocate funds among your approved markets.
- {showProcessModal && } + {showProcessModal && ( + setShowProcessModal(false)} + /> + )}
diff --git a/app/positions/components/agent/Success.tsx b/app/positions/components/agent/Success.tsx index cb9aaa56..b5d85279 100644 --- a/app/positions/components/agent/Success.tsx +++ b/app/positions/components/agent/Success.tsx @@ -6,9 +6,15 @@ const img = require('../../../../src/imgs/agent/agent.png') as string; type SuccessProps = { onClose: () => void; + onDone: () => void; }; -export function Success({ onClose }: SuccessProps) { +export function Success({ onClose, onDone }: SuccessProps) { + const handleDone = () => { + onDone(); + onClose(); + }; + return (
-
-

Beep boop... Command processed!

- - Monarch Agent - -

- Monarch Agent is now ready to help optimize your positions. You can monitor its activity - and performance in the Portfolio dashboard. -

-
+ Success
-
diff --git a/app/positions/components/agent/Welcome.tsx b/app/positions/components/agent/Welcome.tsx index e32e575e..534e4be0 100644 --- a/app/positions/components/agent/Welcome.tsx +++ b/app/positions/components/agent/Welcome.tsx @@ -29,7 +29,7 @@ export function Welcome({ onNext }: WelcomeProps) {
); diff --git a/docs/Styling.md b/docs/Styling.md index 02baaa1c..bfd4f4ec 100644 --- a/docs/Styling.md +++ b/docs/Styling.md @@ -84,10 +84,10 @@ import { Button } from '@/components/common/Button'; ## Tooltip -Use the nextui tooltip with component for consistnet styling +Use the nextui tooltip with component for consistnet styling ``` -} @@ -96,4 +96,4 @@ Use the nextui tooltip with component for consistnet styling />} > -``` \ No newline at end of file +``` diff --git a/src/components/AgentSetupProcessModal.tsx b/src/components/AgentSetupProcessModal.tsx index ab33c511..3b98cd6f 100644 --- a/src/components/AgentSetupProcessModal.tsx +++ b/src/components/AgentSetupProcessModal.tsx @@ -1,10 +1,12 @@ import React from 'react'; +import { Cross1Icon } from '@radix-ui/react-icons'; import { motion, AnimatePresence } from 'framer-motion'; import { FaCheckCircle, FaCircle } from 'react-icons/fa'; import { AuthorizeAgentStep } from '@/hooks/useAuthorizeAgent'; type AgentSetupModalProps = { currentStep: AuthorizeAgentStep; + onClose: () => void; }; const steps = [ @@ -21,7 +23,10 @@ const steps = [ }, ]; -export function AgentSetupProcessModal({ currentStep }: AgentSetupModalProps): JSX.Element { +export function AgentSetupProcessModal({ + currentStep, + onClose, +}: AgentSetupModalProps): JSX.Element { const getStepStatus = (stepKey: string) => { const currentIndex = steps.findIndex((step) => step.key === currentStep); const stepIndex = steps.findIndex((step) => step.key === stepKey); @@ -49,6 +54,14 @@ export function AgentSetupProcessModal({ currentStep }: AgentSetupModalProps): J exit={{ scale: 0.95 }} className="relative w-full max-w-lg rounded bg-white p-4 shadow-xl dark:bg-gray-900" > + +

Setup Monarch Agent

Setup Rebalance market caps

diff --git a/src/hooks/useAuthorizeAgent.ts b/src/hooks/useAuthorizeAgent.ts index 3aac9967..73b62fda 100644 --- a/src/hooks/useAuthorizeAgent.ts +++ b/src/hooks/useAuthorizeAgent.ts @@ -5,7 +5,7 @@ import { useAccount, useReadContract, useSignTypedData, useSwitchChain } from 'w import monarchAgentAbi from '@/abis/monarch-agent-v1'; import morphoAbi from '@/abis/morpho'; import { useTransactionWithToast } from '@/hooks/useTransactionWithToast'; -import { AGENT_CONTRACT, rebalancer } from '@/utils/monarch-agent'; +import { AGENT_CONTRACT } from '@/utils/monarch-agent'; import { MONARCH_TX_IDENTIFIER, MORPHO } from '@/utils/morpho'; import { SupportedNetworks } from '@/utils/networks'; import { Market } from '@/utils/types'; @@ -28,7 +28,11 @@ export type MarketCap = { * @param onSuccess * @returns */ -export const useAuthorizeAgent = (marketCaps: MarketCap[], onSuccess?: () => void) => { +export const useAuthorizeAgent = ( + agent: Address, + marketCaps: MarketCap[], + onSuccess?: () => void, +) => { const [isConfirming, setIsConfirming] = useState(false); const [currentStep, setCurrentStep] = useState(AuthorizeAgentStep.Idle); @@ -158,11 +162,11 @@ export const useAuthorizeAgent = (marketCaps: MarketCap[], onSuccess?: () => voi setCurrentStep(AuthorizeAgentStep.Execute); // add rebalancer if not set yet - if (rebalancerAddress !== rebalancer) { + if (rebalancerAddress !== agent) { const rebalancerTx = encodeFunctionData({ abi: monarchAgentAbi, functionName: 'authorize', - args: [rebalancer], + args: [agent], }); transactions.push(rebalancerTx); } diff --git a/src/utils/monarch-agent.ts b/src/utils/monarch-agent.ts index 5e64c7fb..e4ddc17c 100644 --- a/src/utils/monarch-agent.ts +++ b/src/utils/monarch-agent.ts @@ -1,4 +1,21 @@ +import { AgentMetadata } from './types'; + export const AGENT_CONTRACT = '0x6a9BA5c91fDd608b3F85c3E031a4f531f331f545'; +export enum KnownAgents { + MAX_APY = '0xe0e04468A54937244BEc3bc6C1CA8Bc36ECE6704', + // in the future, add more +} + // v1 rebalancer EOA -export const rebalancer = '0xe0e04468A54937244BEc3bc6C1CA8Bc36ECE6704'; +export const agents: AgentMetadata[] = [ + { + name: 'Max APY Agent', + address: KnownAgents.MAX_APY, + strategyDescription: 'Rebalance every 8 hours, always move to the highest APY', + }, +]; + +export const findAgent = (address: string): AgentMetadata | undefined => { + return agents.find((agent) => agent.address.toLowerCase() === address.toLowerCase()); +}; diff --git a/src/utils/types.ts b/src/utils/types.ts index 8fefe179..c644d267 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -378,3 +378,9 @@ export type UserRebalancerInfo = { transactionHash: string; }[]; }; + +export type AgentMetadata = { + address: Address; + name: string; + strategyDescription: string; +};