From a204b2c65a47d90ed845fa63ba8da9f838a25bbe Mon Sep 17 00:00:00 2001 From: Anton Cheng Date: Mon, 30 Dec 2024 17:30:11 +0800 Subject: [PATCH 1/4] chore: agents --- app/positions/components/agent/Main.tsx | 178 ++++++++++++++++++ app/positions/components/agent/SetupAgent.tsx | 3 +- .../components/agent/SetupAgentModal.tsx | 25 ++- app/positions/components/agent/Welcome.tsx | 2 +- docs/Styling.md | 6 +- src/hooks/useAuthorizeAgent.ts | 8 +- src/utils/monarch-agent.ts | 19 +- src/utils/types.ts | 6 + 8 files changed, 232 insertions(+), 15 deletions(-) create mode 100644 app/positions/components/agent/Main.tsx diff --git a/app/positions/components/agent/Main.tsx b/app/positions/components/agent/Main.tsx new file mode 100644 index 00000000..e4f69978 --- /dev/null +++ b/app/positions/components/agent/Main.tsx @@ -0,0 +1,178 @@ +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 }) => { + const token = findToken(address, chainId); + return ( +
+ {token?.img && ( + {token.symbol} + )} + + {token?.symbol || 'Unknown'} ({markets.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..0734d916 100644 --- a/app/positions/components/agent/SetupAgent.tsx +++ b/app/positions/components/agent/SetupAgent.tsx @@ -12,6 +12,7 @@ import { SupportedNetworks } from '@/utils/networks'; import { OracleVendorIcons, OracleVendors } from '@/utils/oracle'; import { findToken } from '@/utils/tokens'; import { Market, MarketPosition, UserRebalancerInfo } from '@/utils/types'; +import { KnownAgents } from '@/utils/monarch-agent'; type MarketGroup = { loanAsset: { @@ -171,7 +172,7 @@ export function SetupAgent({ ); }; - const { executeBatchSetupAgent, currentStep } = useAuthorizeAgent(pendingCaps, onNext); + const { executeBatchSetupAgent, currentStep } = useAuthorizeAgent(KnownAgents.MAX_APY, pendingCaps, onNext); const handleExecute = useCallback(() => { setShowProcessModal(true); diff --git a/app/positions/components/agent/SetupAgentModal.tsx b/app/positions/components/agent/SetupAgentModal.tsx index 25f36e5e..8af0c8e0 100644 --- a/app/positions/components/agent/SetupAgentModal.tsx +++ b/app/positions/components/agent/SetupAgentModal.tsx @@ -5,21 +5,23 @@ import { Address } from 'viem'; import { useMarkets } from '@/contexts/MarketsContext'; import { MarketCap } from '@/hooks/useAuthorizeAgent'; import useUserPositions from '@/hooks/useUserPositions'; +import { findAgent } from '@/utils/monarch-agent'; import { SupportedNetworks } from '@/utils/networks'; import { Market, UserRebalancerInfo } from '@/utils/types'; +import { Main as MainContent } from './Main'; import { SetupAgent } from './SetupAgent'; import { Success as SuccessContent } from './Success'; import { Welcome as WelcomeContent } from './Welcome'; export enum SetupStep { - Welcome = 'welcome', + Main = 'main', Setup = 'setup', Success = 'success', } const SETUP_STEPS = [ { - id: SetupStep.Welcome, + id: SetupStep.Main, title: 'Welcome to Monarch Agent', description: 'Bee-bee-bee, Monarch Agent is here!', }, @@ -73,10 +75,10 @@ export function SetupAgentModal({ onClose, userRebalancerInfo, }: SetupAgentModalProps) { - const [currentStep, setCurrentStep] = useState(SetupStep.Welcome); + const [currentStep, setCurrentStep] = useState(SetupStep.Main); const [pendingCaps, setPendingCaps] = useState([]); - const { data: positions } = useUserPositions(account, true); + const { data: positions, history } = useUserPositions(account, true); const { markets: allMarkets } = useMarkets(); @@ -110,6 +112,9 @@ export function SetupAgentModal({ setPendingCaps((prev) => prev.filter((cap) => cap.market.uniqueKey !== market.uniqueKey)); }; + const hasSetupAgent = + !!userRebalancerInfo && findAgent(userRebalancerInfo.rebalancer) !== undefined; + return ( {/* Step Content */} - {currentStep === SetupStep.Welcome && } + {currentStep === SetupStep.Main && !hasSetupAgent && ( + + )} + {currentStep === SetupStep.Main && hasSetupAgent && ( + + )} {currentStep === SetupStep.Setup && (
); 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/hooks/useAuthorizeAgent.ts b/src/hooks/useAuthorizeAgent.ts index 3aac9967..5a2aeb38 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,7 @@ 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 +158,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; +}; From b13a1b5412010ebdaaa3fa6d44ac1e64123b3ff1 Mon Sep 17 00:00:00 2001 From: Anton Cheng Date: Mon, 30 Dec 2024 17:55:03 +0800 Subject: [PATCH 2/4] chore: onclose --- app/positions/components/PositionsContent.tsx | 7 ++-- app/positions/components/agent/Main.tsx | 6 ++-- app/positions/components/agent/SetupAgent.tsx | 23 +++++++++--- .../components/agent/SetupAgentModal.tsx | 36 +++++++++++++------ app/positions/components/agent/Success.tsx | 35 +++++++++--------- src/components/AgentSetupProcessModal.tsx | 11 +++++- src/hooks/useAuthorizeAgent.ts | 6 +++- 7 files changed, 85 insertions(+), 39 deletions(-) diff --git a/app/positions/components/PositionsContent.tsx b/app/positions/components/PositionsContent.tsx index 8ef970da..93589773 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={() => { + 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 index e4f69978..d17f1fcc 100644 --- a/app/positions/components/agent/Main.tsx +++ b/app/positions/components/agent/Main.tsx @@ -110,7 +110,7 @@ export function Main({ account, onNext, userRebalancerInfo, history }: MainProps

Monitoring Positions

- {Object.values(loanAssetGroups).map(({ address, chainId, markets }) => { + {Object.values(loanAssetGroups).map(({ address, chainId, markets: authorizedMarkets }) => { const token = findToken(address, chainId); return (
)} - {token?.symbol || 'Unknown'} ({markets.length}) + {token?.symbol ?? 'Unknown'} ({authorizedMarkets.length})
); @@ -161,7 +161,7 @@ export function Main({ account, onNext, userRebalancerInfo, history }: MainProps rel="noopener noreferrer" className="bg-surface flex items-center gap-2 rounded px-3 py-2 text-sm no-underline" > - View All ({userRebalancerInfo.transactions?.length || 0}) + View All ({userRebalancerInfo.transactions?.length ?? 0}) )} diff --git a/app/positions/components/agent/SetupAgent.tsx b/app/positions/components/agent/SetupAgent.tsx index 0734d916..8e35e8d3 100644 --- a/app/positions/components/agent/SetupAgent.tsx +++ b/app/positions/components/agent/SetupAgent.tsx @@ -8,11 +8,11 @@ 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'; import { Market, MarketPosition, UserRebalancerInfo } from '@/utils/types'; -import { KnownAgents } from '@/utils/monarch-agent'; type MarketGroup = { loanAsset: { @@ -172,7 +172,16 @@ export function SetupAgent({ ); }; - const { executeBatchSetupAgent, currentStep } = useAuthorizeAgent(KnownAgents.MAX_APY, 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); @@ -181,8 +190,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 markets you authorize it to!
- {showProcessModal && } + {showProcessModal && setShowProcessModal(false)} />}
diff --git a/app/positions/components/agent/Success.tsx b/app/positions/components/agent/Success.tsx index cb9aaa56..1c9d7ed0 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/src/components/AgentSetupProcessModal.tsx b/src/components/AgentSetupProcessModal.tsx index ab33c511..eed3be3e 100644 --- a/src/components/AgentSetupProcessModal.tsx +++ b/src/components/AgentSetupProcessModal.tsx @@ -1,10 +1,12 @@ import React from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { FaCheckCircle, FaCircle } from 'react-icons/fa'; +import { Cross1Icon } from '@radix-ui/react-icons'; import { AuthorizeAgentStep } from '@/hooks/useAuthorizeAgent'; type AgentSetupModalProps = { currentStep: AuthorizeAgentStep; + onClose: () => void; }; const steps = [ @@ -21,7 +23,7 @@ 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 +51,13 @@ 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 5a2aeb38..73b62fda 100644 --- a/src/hooks/useAuthorizeAgent.ts +++ b/src/hooks/useAuthorizeAgent.ts @@ -28,7 +28,11 @@ export type MarketCap = { * @param onSuccess * @returns */ -export const useAuthorizeAgent = (agent: Address,marketCaps: MarketCap[], onSuccess?: () => void) => { +export const useAuthorizeAgent = ( + agent: Address, + marketCaps: MarketCap[], + onSuccess?: () => void, +) => { const [isConfirming, setIsConfirming] = useState(false); const [currentStep, setCurrentStep] = useState(AuthorizeAgentStep.Idle); From a6787218bd2e96a5b32f6e5090977ddf20597e21 Mon Sep 17 00:00:00 2001 From: Anton Cheng Date: Mon, 30 Dec 2024 18:04:36 +0800 Subject: [PATCH 3/4] chore: refetch --- app/positions/components/PositionsContent.tsx | 4 +- app/positions/components/agent/Main.tsx | 46 ++++++++++--------- app/positions/components/agent/SetupAgent.tsx | 12 +++-- app/positions/components/agent/Success.tsx | 4 +- src/components/AgentSetupProcessModal.tsx | 10 ++-- 5 files changed, 44 insertions(+), 32 deletions(-) diff --git a/app/positions/components/PositionsContent.tsx b/app/positions/components/PositionsContent.tsx index 93589773..14919508 100644 --- a/app/positions/components/PositionsContent.tsx +++ b/app/positions/components/PositionsContent.tsx @@ -158,8 +158,8 @@ export default function Positions() { { - refetchRebalancerInfo() - setShowSetupAgentModal(false) + 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 index d17f1fcc..260511d3 100644 --- a/app/positions/components/agent/Main.tsx +++ b/app/positions/components/agent/Main.tsx @@ -110,28 +110,30 @@ export function Main({ account, onNext, userRebalancerInfo, history }: MainProps

Monitoring Positions

- {Object.values(loanAssetGroups).map(({ address, chainId, markets: authorizedMarkets }) => { - const token = findToken(address, chainId); - return ( -
- {token?.img && ( - {token.symbol} - )} - - {token?.symbol ?? 'Unknown'} ({authorizedMarkets.length}) - -
- ); - })} + {Object.values(loanAssetGroups).map( + ({ address, chainId, markets: marketsForLoanAsset }) => { + const token = findToken(address, chainId); + return ( +
+ {token?.img && ( + {token.symbol} + )} + + {token?.symbol ?? 'Unknown'} ({marketsForLoanAsset.length}) + +
+ ); + }, + )}
diff --git a/app/positions/components/agent/SetupAgent.tsx b/app/positions/components/agent/SetupAgent.tsx index 8e35e8d3..2f8ba4e6 100644 --- a/app/positions/components/agent/SetupAgent.tsx +++ b/app/positions/components/agent/SetupAgent.tsx @@ -172,7 +172,8 @@ export function SetupAgent({ ); }; - const hasSetupAgent = userRebalancerInfo?.rebalancer.toLowerCase() === KnownAgents.MAX_APY.toLowerCase(); + const hasSetupAgent = + userRebalancerInfo?.rebalancer.toLowerCase() === KnownAgents.MAX_APY.toLowerCase(); // todo: search user agent after const agent = findAgent(KnownAgents.MAX_APY ?? ''); @@ -193,7 +194,7 @@ export function SetupAgent({ {!hasSetupAgent && agent && (

{agent.name}

-

{agent.strategyDescription}

+

{agent.strategyDescription}

)}
@@ -226,7 +227,12 @@ export function SetupAgent({ key={groupKey} className="overflow-hidden rounded border border-divider bg-content1" > - {showProcessModal && setShowProcessModal(false)} />} + {showProcessModal && ( + setShowProcessModal(false)} + /> + )} From 5a61d3c5a736571f70905d6fbc1c8938d40aafb5 Mon Sep 17 00:00:00 2001 From: Anton Cheng Date: Mon, 30 Dec 2024 18:32:18 +0800 Subject: [PATCH 4/4] chore: wording --- app/positions/components/agent/SetupAgent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/positions/components/agent/SetupAgent.tsx b/app/positions/components/agent/SetupAgent.tsx index 2f8ba4e6..48f0da1f 100644 --- a/app/positions/components/agent/SetupAgent.tsx +++ b/app/positions/components/agent/SetupAgent.tsx @@ -198,7 +198,7 @@ export function SetupAgent({
)}
- The agent can only reallocate funds among markets you authorize it to! + The agent can only reallocate funds among your approved markets.