diff --git a/README.md b/README.md index 49a32d0..6503e5b 100644 --- a/README.md +++ b/README.md @@ -23,3 +23,19 @@ npm run dev Client: http://localhost:5173 Server API: http://localhost:4000/api + +## Frontend quickstart (CashflowVaults dashboard) + +This repo includes a Next.js App Router dashboard at `/cashflow` for the Avalanche Fuji CashflowVaults deployment. + +```bash +# install frontend dependencies if needed +npm install + +# run next dev server (if your setup uses a dedicated Next command) +npm run next:dev +``` + +Then open: `http://localhost:3000/cashflow`. + +> Note: if your local scripts currently run the Vite + Express demo, add/use a Next.js dev script that starts this app directory. diff --git a/app/cashflow/page.tsx b/app/cashflow/page.tsx new file mode 100644 index 0000000..36cb2c4 --- /dev/null +++ b/app/cashflow/page.tsx @@ -0,0 +1,5 @@ +import CashflowDashboard from "@/components/CashflowDashboard"; + +export default function CashflowPage() { + return ; +} diff --git a/app/lib/abi/CashflowSchedule.json b/app/lib/abi/CashflowSchedule.json new file mode 100644 index 0000000..86ec52e --- /dev/null +++ b/app/lib/abi/CashflowSchedule.json @@ -0,0 +1,190 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "creator_", + "type": "address", + "internalType": "address" + }, + { + "name": "vault_", + "type": "address", + "internalType": "address" + }, + { + "name": "cadence_", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "paymentAmount_", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "startTime_", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "cadence", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "creator", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "end", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "lastPaymentTime", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "pause", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "pay", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "paymentAmount", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "resume", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "startTime", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "state", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint8", + "internalType": "enum CashflowSchedule.State" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "vault", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract CashflowVault" + } + ], + "stateMutability": "view" + }, + { + "type": "event", + "name": "PaymentExecuted", + "inputs": [ + { + "name": "creator", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "paymentTime", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "StateChanged", + "inputs": [ + { + "name": "newState", + "type": "uint8", + "indexed": true, + "internalType": "enum CashflowSchedule.State" + } + ], + "anonymous": false + } +] diff --git a/app/lib/abi/CashflowShareNFT.json b/app/lib/abi/CashflowShareNFT.json new file mode 100644 index 0000000..b8ec871 --- /dev/null +++ b/app/lib/abi/CashflowShareNFT.json @@ -0,0 +1,190 @@ +[ + { + "type": "function", + "name": "balanceOf", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "hasReceipt", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "mintFirstDepositReceipt", + "inputs": [ + { + "name": "investor", + "type": "address", + "internalType": "address" + }, + { + "name": "scheduleId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "metadataUri", + "type": "string", + "internalType": "string" + } + ], + "outputs": [ + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "name", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "nextTokenId", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "ownerOf", + "inputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "scheduleIdOf", + "inputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "symbol", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "tokenURI", + "inputs": [ + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "uri", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "event", + "name": "Transfer", + "inputs": [ + { + "name": "from", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + } + ], + "anonymous": false + } +] diff --git a/app/lib/abi/CashflowVault.json b/app/lib/abi/CashflowVault.json new file mode 100644 index 0000000..990a650 --- /dev/null +++ b/app/lib/abi/CashflowVault.json @@ -0,0 +1,625 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "asset_", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "admin", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "allowance", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "approve", + "inputs": [ + { + "name": "spender", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "asset", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract MockUSDC" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "balanceOf", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "convertToAssets", + "inputs": [ + { + "name": "shares", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "assets", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "convertToShares", + "inputs": [ + { + "name": "assets", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "shares", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "decimals", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint8", + "internalType": "uint8" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "deposit", + "inputs": [ + { + "name": "assets", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "receiver", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "shares", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "mint", + "inputs": [ + { + "name": "shares", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "receiver", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "assets", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "name", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "previewDeposit", + "inputs": [ + { + "name": "assets", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "shares", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "previewMint", + "inputs": [ + { + "name": "shares", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "assets", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "previewRedeem", + "inputs": [ + { + "name": "shares", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "assets", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "previewWithdraw", + "inputs": [ + { + "name": "assets", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "shares", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "recordCashflowPayment", + "inputs": [ + { + "name": "payer", + "type": "address", + "internalType": "address" + }, + { + "name": "assets", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "redeem", + "inputs": [ + { + "name": "shares", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "receiver", + "type": "address", + "internalType": "address" + }, + { + "name": "owner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "assets", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "schedule", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "setSchedule", + "inputs": [ + { + "name": "schedule_", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "symbol", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "totalAssets", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "totalSupply", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "transfer", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferFrom", + "inputs": [ + { + "name": "from", + "type": "address", + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "withdraw", + "inputs": [ + { + "name": "assets", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "receiver", + "type": "address", + "internalType": "address" + }, + { + "name": "owner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "shares", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "Approval", + "inputs": [ + { + "name": "owner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "spender", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "CashflowPaymentRecorded", + "inputs": [ + { + "name": "payer", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "assets", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Deposit", + "inputs": [ + { + "name": "caller", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "receiver", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "assets", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "shares", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Transfer", + "inputs": [ + { + "name": "from", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Withdraw", + "inputs": [ + { + "name": "caller", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "receiver", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "owner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "assets", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "shares", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + } +] diff --git a/app/lib/abi/MockUSDC.json b/app/lib/abi/MockUSDC.json new file mode 100644 index 0000000..9dad6d6 --- /dev/null +++ b/app/lib/abi/MockUSDC.json @@ -0,0 +1,242 @@ +[ + { + "type": "function", + "name": "allowance", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "approve", + "inputs": [ + { + "name": "spender", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "balanceOf", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "decimals", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint8", + "internalType": "uint8" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "mint", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "name", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "symbol", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "totalSupply", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "transfer", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferFrom", + "inputs": [ + { + "name": "from", + "type": "address", + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "Approval", + "inputs": [ + { + "name": "owner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "spender", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Transfer", + "inputs": [ + { + "name": "from", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + } +] diff --git a/app/lib/cashflowContracts.ts b/app/lib/cashflowContracts.ts new file mode 100644 index 0000000..4e17d89 --- /dev/null +++ b/app/lib/cashflowContracts.ts @@ -0,0 +1,29 @@ +import mockUsdcAbi from "./abi/MockUSDC.json"; +import cashflowVaultAbi from "./abi/CashflowVault.json"; +import cashflowScheduleAbi from "./abi/CashflowSchedule.json"; +import cashflowShareNftAbi from "./abi/CashflowShareNFT.json"; + +export const cashflowChainId = 43113; + +export const cashflowContracts = { + mockUsdc: { + address: "0x76491A6B6AAEB808fA0CE1A5041F4998244DE6F2" as const, + abi: mockUsdcAbi + }, + vault: { + address: "0xE8665936630608a077fF78204F45167A7F6b52A3" as const, + abi: cashflowVaultAbi + }, + schedule: { + address: "0x7f09A950FD3B011F5d9ABd15d1e775481f7F313C" as const, + abi: cashflowScheduleAbi + }, + shareNft: { + address: "0x7C693ae7016bd786612430d654D82DC80114d041" as const, + abi: cashflowShareNftAbi + } +}; + +export const fujiRpcUrl = + process.env.NEXT_PUBLIC_RPC_URL ?? + "https://api.avax-test.network/ext/bc/C/rpc"; diff --git a/app/providers.tsx b/app/providers.tsx index 2979cc4..911bbe1 100644 --- a/app/providers.tsx +++ b/app/providers.tsx @@ -5,10 +5,7 @@ import { createConfig, http, WagmiProvider } from "wagmi"; import { avalancheFuji } from "wagmi/chains"; import { injected } from "wagmi/connectors"; import { useMemo } from "react"; - -const defaultRpc = - process.env.NEXT_PUBLIC_RPC_URL ?? - "https://api.avax-test.network/ext/bc/C/rpc"; +import { cashflowChainId, fujiRpcUrl } from "@/app/lib/cashflowContracts"; export default function Providers({ children }: { children: React.ReactNode }) { const queryClient = useMemo(() => new QueryClient(), []); @@ -18,7 +15,7 @@ export default function Providers({ children }: { children: React.ReactNode }) { chains: [avalancheFuji], connectors: [injected()], transports: { - [avalancheFuji.id]: http(defaultRpc) + [cashflowChainId]: http(fujiRpcUrl) } }), [] @@ -26,9 +23,7 @@ export default function Providers({ children }: { children: React.ReactNode }) { return ( - - {children} - + {children} ); } diff --git a/components/CashflowDashboard.tsx b/components/CashflowDashboard.tsx new file mode 100644 index 0000000..280b207 --- /dev/null +++ b/components/CashflowDashboard.tsx @@ -0,0 +1,174 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { useAccount, useChainId, useConnect, useDisconnect, usePublicClient, useReadContract, useSwitchChain, useWriteContract } from "wagmi"; +import { parseUnits, formatUnits } from "viem"; +import { avalancheFuji } from "wagmi/chains"; +import { cashflowChainId, cashflowContracts } from "@/app/lib/cashflowContracts"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { ActionsPanel } from "@/components/cashflow/ActionsPanel"; +import { VaultOverview } from "@/components/cashflow/VaultOverview"; +import { DemoChecklist } from "@/components/cashflow/DemoChecklist"; +import { PpsChart, PpsPoint } from "@/components/cashflow/PpsChart"; +import type { ToastItem } from "@/components/cashflow/types"; + +const usdcDecimals = 6; +const mintExists = cashflowContracts.mockUsdc.abi.some((item) => item.type === "function" && item.name === "mint"); + +export default function CashflowDashboard() { + const { address, isConnected } = useAccount(); + const { connectors, connect } = useConnect(); + const { disconnect } = useDisconnect(); + const { switchChain } = useSwitchChain(); + const chainId = useChainId(); + const isFuji = chainId === cashflowChainId; + const publicClient = usePublicClient(); + const { writeContractAsync } = useWriteContract(); + + const [depositAmount, setDepositAmount] = useState(""); + const [redeemShares, setRedeemShares] = useState(""); + const [toasts, setToasts] = useState([]); + const [ppsPoints, setPpsPoints] = useState([]); + + const pushToast = (type: ToastItem["type"], message: string) => { + const id = Date.now() + Math.random(); + setToasts((prev) => [...prev, { id, type, message }]); + setTimeout(() => setToasts((prev) => prev.filter((t) => t.id !== id)), 4500); + }; + + const { data: totalAssets } = useReadContract({ ...cashflowContracts.vault, functionName: "totalAssets" }); + const { data: totalSupply } = useReadContract({ ...cashflowContracts.vault, functionName: "totalSupply" }); + const { data: userShares } = useReadContract({ ...cashflowContracts.vault, functionName: "balanceOf", args: [address!], query: { enabled: !!address } }); + const { data: userClaimableAssets } = useReadContract({ ...cashflowContracts.vault, functionName: "convertToAssets", args: [userShares ?? 0n], query: { enabled: userShares !== undefined } }); + const { data: creator } = useReadContract({ ...cashflowContracts.schedule, functionName: "creator" }); + const { data: usdcAllowanceToVault } = useReadContract({ ...cashflowContracts.mockUsdc, functionName: "allowance", args: [address!, cashflowContracts.vault.address], query: { enabled: !!address } }); + const { data: usdcAllowanceToSchedule } = useReadContract({ ...cashflowContracts.mockUsdc, functionName: "allowance", args: [address!, cashflowContracts.schedule.address], query: { enabled: !!address } }); + const { data: usdcBalance } = useReadContract({ ...cashflowContracts.mockUsdc, functionName: "balanceOf", args: [address!], query: { enabled: !!address } }); + + const isCreator = !!address && !!creator && address.toLowerCase() === creator.toLowerCase(); + + const runTx = async (label: string, tx: () => Promise<`0x${string}`>) => { + try { + pushToast("pending", `${label} pending...`); + const hash = await tx(); + await publicClient?.waitForTransactionReceipt({ hash }); + pushToast("success", `${label} confirmed`); + } catch (error) { + pushToast("error", `${label} failed: ${error instanceof Error ? error.message : "Unknown error"}`); + } + }; + + // PPS = totalAssets / totalSupply. Vault and USDC both use 6 decimals so ratio can be displayed directly. + useEffect(() => { + if (totalAssets === undefined || totalSupply === undefined) return; + const timer = setInterval(async () => { + const assets = (await publicClient?.readContract({ ...cashflowContracts.vault, functionName: "totalAssets" })) as bigint | undefined; + const supply = (await publicClient?.readContract({ ...cashflowContracts.vault, functionName: "totalSupply" })) as bigint | undefined; + const pps = supply && supply > 0n && assets !== undefined ? Number(assets) / Number(supply) : 0; + setPpsPoints((prev) => [...prev.slice(-24), { label: new Date().toLocaleTimeString(), pps }]); + }, 12000); + return () => clearInterval(timer); + }, [publicClient, totalAssets, totalSupply]); + + useEffect(() => { + if (totalAssets === undefined || totalSupply === undefined) return; + const pps = totalSupply > 0n ? Number(totalAssets) / Number(totalSupply) : 0; + setPpsPoints((prev) => (prev.length ? prev : [{ label: "Now", pps }])); + }, [totalAssets, totalSupply]); + + const steps = useMemo( + () => [ + { label: "Connect wallet", done: isConnected }, + { label: "Mint mock USDC", done: (usdcBalance ?? 0n) > 0n }, + { label: "Approve Vault", done: (usdcAllowanceToVault ?? 0n) > 0n }, + { label: "Deposit", done: (userShares ?? 0n) > 0n }, + { label: "Creator approve + pay twice", done: isCreator ? (usdcAllowanceToSchedule ?? 0n) > 0n && ppsPoints.length >= 3 : true }, + { label: "Redeem", done: (userShares ?? 0n) === 0n && (userClaimableAssets ?? 0n) === 0n } + ], + [isConnected, usdcBalance, usdcAllowanceToVault, userShares, isCreator, usdcAllowanceToSchedule, ppsPoints.length, userClaimableAssets] + ); + + return ( +
+
+
+

CashflowVaults • Avalanche Fuji

+

Tokenize future cashflows into yield-bearing vault shares.

+

Deposit USDC into an ERC-4626 vault, trigger scheduled creator payments that increase price per share, and redeem shares for realized profits.

+ +
+ {!isConnected ? ( + + ) : ( + <> + {address?.slice(0, 6)}...{address?.slice(-4)} + + + )} + Network: {isFuji ? "Avalanche Fuji" : `Chain ${chainId}`} + {!isFuji && isConnected && ( + + )} +
+
+ + + +
+
+ +

Explorer Links

{Object.entries(cashflowContracts).map(([k, c]) => ({k} • {c.address}))}
+
+ +
+ + void runTx("Approve vault", () => writeContractAsync({ ...cashflowContracts.mockUsdc, functionName: "approve", args: [cashflowContracts.vault.address, parseUnits("1000000000", usdcDecimals)] })) + } + onDeposit={() => + void runTx("Deposit", () => { + const amount = parseUnits(depositAmount || "0", usdcDecimals); + return writeContractAsync({ ...cashflowContracts.vault, functionName: "deposit", args: [amount, address!] }); + }) + } + onRedeem={() => + void runTx("Redeem", () => { + const shares = parseUnits(redeemShares || "0", usdcDecimals); + return writeContractAsync({ ...cashflowContracts.vault, functionName: "redeem", args: [shares, address!, address!] }); + }) + } + onMaxRedeem={() => setRedeemShares(formatUnits(userShares ?? 0n, usdcDecimals))} + onApproveSchedule={() => + void runTx("Approve schedule", () => writeContractAsync({ ...cashflowContracts.mockUsdc, functionName: "approve", args: [cashflowContracts.schedule.address, parseUnits("1000000000", usdcDecimals)] })) + } + onPay={() => void runTx("Schedule pay", () => writeContractAsync({ ...cashflowContracts.schedule, functionName: "pay" }))} + /> + + + + {mintExists && ( + + )} +
+
+
+ +
+ {toasts.map((toast) => ( +
+ {toast.message} +
+ ))} +
+
+ ); +} diff --git a/components/cashflow/ActionsPanel.tsx b/components/cashflow/ActionsPanel.tsx new file mode 100644 index 0000000..ef80600 --- /dev/null +++ b/components/cashflow/ActionsPanel.tsx @@ -0,0 +1,50 @@ +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; + +type Props = { + isCreator: boolean; + isFuji: boolean; + onApproveVault: () => void; + onDeposit: () => void; + onRedeem: () => void; + onMaxRedeem: () => void; + onApproveSchedule: () => void; + onPay: () => void; + depositAmount: string; + setDepositAmount: (v: string) => void; + redeemShares: string; + setRedeemShares: (v: string) => void; +}; + +export function ActionsPanel(props: Props) { + return ( + + +
+

Actions

+

Approve, deposit, redeem, and trigger scheduled cashflow payments.

+
+ +
+ +
+ props.setDepositAmount(e.target.value)} /> + +
+
+ props.setRedeemShares(e.target.value)} /> + +
+ +
+ +
+ + + {!props.isCreator &&

Only schedule creator can call pay().

} +
+
+
+ ); +} diff --git a/components/cashflow/DemoChecklist.tsx b/components/cashflow/DemoChecklist.tsx new file mode 100644 index 0000000..6d6a042 --- /dev/null +++ b/components/cashflow/DemoChecklist.tsx @@ -0,0 +1,21 @@ +import { Card, CardContent } from "@/components/ui/card"; + +type Step = { label: string; done: boolean }; + +export function DemoChecklist({ steps }: { steps: Step[] }) { + return ( + + +

Demo Mode

+
    + {steps.map((step) => ( +
  • + {step.label} + {step.done ? "Done" : "Pending"} +
  • + ))} +
+
+
+ ); +} diff --git a/components/cashflow/PpsChart.tsx b/components/cashflow/PpsChart.tsx new file mode 100644 index 0000000..1b26a71 --- /dev/null +++ b/components/cashflow/PpsChart.tsx @@ -0,0 +1,25 @@ +import { Card, CardContent } from "@/components/ui/card"; +import { Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; + +export type PpsPoint = { label: string; pps: number }; + +export function PpsChart({ points }: { points: PpsPoint[] }) { + return ( + + +

Price Per Share Trend

+

Lightweight client-side polling snapshots while this page is open.

+
+ + + + + + + + +
+
+
+ ); +} diff --git a/components/cashflow/VaultOverview.tsx b/components/cashflow/VaultOverview.tsx new file mode 100644 index 0000000..910e194 --- /dev/null +++ b/components/cashflow/VaultOverview.tsx @@ -0,0 +1,24 @@ +import { Card, CardContent } from "@/components/ui/card"; +import { formatUnits } from "viem"; + +type Props = { + totalAssets?: bigint; + totalSupply?: bigint; + userShares?: bigint; + userClaimableAssets?: bigint; +}; + +const fmt = (value?: bigint) => (value !== undefined ? Number(formatUnits(value, 6)).toLocaleString(undefined, { maximumFractionDigits: 4 }) : "-"); + +export function VaultOverview({ totalAssets, totalSupply, userShares, userClaimableAssets }: Props) { + const pps = totalSupply && totalSupply > 0n && totalAssets !== undefined ? Number(totalAssets) / Number(totalSupply) : 0; + + return ( +
+

Total Assets (USDC)

{fmt(totalAssets)}

+

Total Supply (Shares)

{fmt(totalSupply)}

+

Price Per Share

{pps.toFixed(6)}

+

Your Shares / Claimable

{fmt(userShares)} / {fmt(userClaimableAssets)}

+
+ ); +} diff --git a/components/cashflow/types.ts b/components/cashflow/types.ts new file mode 100644 index 0000000..d1f20bc --- /dev/null +++ b/components/cashflow/types.ts @@ -0,0 +1,5 @@ +export type ToastItem = { + id: number; + type: "pending" | "success" | "error"; + message: string; +}; diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000..d50b90c --- /dev/null +++ b/components/ui/button.tsx @@ -0,0 +1,17 @@ +import * as React from "react"; + +type Props = React.ButtonHTMLAttributes & { + variant?: "default" | "outline" | "ghost" | "destructive"; +}; + +export function Button({ variant = "default", className = "", ...props }: Props) { + const base = + "inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium transition disabled:cursor-not-allowed disabled:opacity-50"; + const variants = { + default: "bg-emerald-400 text-slate-950 hover:bg-emerald-300", + outline: "border border-white/20 bg-white/5 text-white hover:bg-white/10", + ghost: "text-slate-200 hover:bg-white/10", + destructive: "bg-rose-500 text-white hover:bg-rose-400" + }; + return