diff --git a/packages/nextjs/app/borrow/page.tsx b/packages/nextjs/app/borrow/page.tsx new file mode 100644 index 0000000..0807d7f --- /dev/null +++ b/packages/nextjs/app/borrow/page.tsx @@ -0,0 +1,133 @@ +"use client"; + +import { useCallback } from "react"; +import { useAccount } from "wagmi"; +import { SkipTimeComponent } from "~~/components/dev/SkipTimeComponent"; +import { useScaffoldWriteContract } from "~~/hooks/scaffold-eth"; +import { useAgreements } from "~~/hooks/useAgreements"; + +export default function BorrowPage() { + const { address: connectedAddress } = useAccount(); + const { agreements, loading, reload: reloadAgreements } = useAgreements(); + + const { writeContractAsync: writeRentToOwnAsync } = useScaffoldWriteContract("RentToOwn"); + + const startAgreement = useCallback(async (id: number, monthlyPayment: bigint) => { + try { + await writeRentToOwnAsync({ + functionName: "startAgreement", + args: [id as unknown as bigint], + value: monthlyPayment, + }); + alert("Agreement started successfully!"); + reloadAgreements(); + } catch (e) { + console.log({ e }); + alert("Failed to start agreement. Please try again."); + } + }, []); + + const makePayment = useCallback(async (id: number, monthlyPayment: bigint) => { + try { + await writeRentToOwnAsync({ + functionName: "makePayment", + args: [id as unknown as bigint], + value: monthlyPayment, + }); + alert("Payment made successfully!"); + reloadAgreements(); + } catch (e) { + console.log({ e }); + alert("Failed to make payment. Please try again."); + } + }, []); + + return ( +
+

NFT Rent-to-Own Agreements

+ + + +

Available Agreements

+ {loading ? ( +

Loading agreements...

+ ) : ( +
+ {agreements + .filter(a => a.isActive && a.borrower === "0x0000000000000000000000000000000000000000") + .map(agreement => ( +
+

Agreement #{agreement.id}

+
+

+ NFT Contract: {agreement.nftContract} +

+

+ NFT ID: {agreement.nftId} +

+

+ Monthly Payment: {agreement.monthlyPayment} ETH +

+

+ Total Price: {agreement.totalPrice} ETH +

+
+ + +
+ ))} +
+ )} + +

My Active Agreements

+ {loading ? ( +

Loading agreements...

+ ) : ( +
+ {agreements + .filter(a => a.isActive && a.borrower.toLowerCase() === (connectedAddress as string)) + .map(agreement => ( +
+

Agreement #{agreement.id}

+
+

+ NFT Contract: {agreement.nftContract} +

+

+ NFT ID: {agreement.nftId} +

+

+ Monthly Payment: {agreement.monthlyPayment} ETH +

+

+ Total Price: {agreement.totalPrice} ETH +

+

+ Total Paid: {agreement.totalPaid} ETH +

+

+ Next Payment Due: {agreement.nextPaymentDue} +

+

+ Remaining: {agreement.totalRemaining} ETH +

+
+ + +
+ ))} +
+ )} +
+ ); +} diff --git a/packages/nextjs/app/lend/page.tsx b/packages/nextjs/app/lend/page.tsx new file mode 100644 index 0000000..8acda7e --- /dev/null +++ b/packages/nextjs/app/lend/page.tsx @@ -0,0 +1,230 @@ +"use client"; + +import React, { useCallback, useState } from "react"; +import { parseEther } from "viem"; +import { useAccount, useWriteContract } from "wagmi"; +import { useScaffoldContract, useTransactor } from "~~/hooks/scaffold-eth"; +import { useAllContracts } from "~~/utils/scaffold-eth/contractsData"; + +// Define the NFT type +interface NFT { + name: string; + tokenId: bigint; + contractAddress: string; // Add this property based on your NFT structure +} + +const RentToOwnPage = () => { + const { address: connectedAddress } = useAccount(); + const { RentToOwn, MyNFT } = useAllContracts(); + + const [nfts, setNfts] = useState([]); // Specify the type for nfts + const [selectedNft, setSelectedNft] = useState(null); // Specify the type for selectedNft + const [monthlyPayment, setMonthlyPayment] = useState(""); + const [numberOfPayments, setNumberOfPayments] = useState(""); + const [contractAddress, setContractAddress] = useState(""); // State for contract address input + + const { data: myNFTContract, isLoading: myNFTIsLoading } = useScaffoldContract({ + contractName: "MyNFT", + }); + const { writeContractAsync } = useWriteContract(); + const writeTx = useTransactor(); + + const loadNFTs = async (contractAddress: string): Promise => { + if (myNFTIsLoading || !myNFTContract) { + alert("The NFT Contract is loading."); + return []; + } + // Get the current token ID + const currentTokenId = await myNFTContract.read.getCurrentTokenId(); + console.log("Current token ID:", currentTokenId.toString()); + + // Create NFT object + const nfts: NFT[] = [ + { + name: await myNFTContract.read.name(), + tokenId: currentTokenId, + contractAddress: contractAddress, + }, + ]; + + console.log("Found NFTs:", nfts); + return nfts; + }; + + const handleLoadNFTs = useCallback(async () => { + if (!contractAddress) { + alert("Please enter a valid contract address."); + return; + } + + try { + const nfts = await loadNFTs(contractAddress); + setNfts(nfts); + } catch (error) { + console.error("Error loading NFTs:", error); + alert("Failed to load NFTs. Check console for details."); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [contractAddress, setContractAddress]); + + const handleLendNFT = useCallback(async () => { + if (myNFTIsLoading || !myNFTContract) { + alert("The NFT Contract is loading."); + return []; + } + if (!selectedNft || !monthlyPayment || !numberOfPayments) { + alert("Please fill in all fields"); + return; + } + + try { + // 1. First verify the NFT contract + // 2. Check if user owns the NFT + const owner = await myNFTContract.read.ownerOf(selectedNft.tokenId as any); + if (owner.toLowerCase() !== connectedAddress) { + alert("You do not own this NFT"); + return; + } + + // 3. Get the RentToOwn contract using the constant + // 4. Approve the NFT transfer + console.log("Approving NFT transfer..."); + const writeApproveNFTTransfer = () => + writeContractAsync({ + address: MyNFT.address, + abi: MyNFT.abi, + functionName: "approve", + args: [RentToOwn.address, selectedNft.tokenId], + }); + const approveTx = await writeTx(writeApproveNFTTransfer, { blockConfirmations: 1 }); + console.log("Approval transaction:", approveTx); + + // 5. List the NFT + console.log("Listing NFT with parameters:", { + nftContract: selectedNft.contractAddress, + tokenId: selectedNft.tokenId, + monthlyPayment, + numberOfPayments, + }); + + const writeListNFT = () => + writeContractAsync({ + address: RentToOwn.address, + abi: RentToOwn.abi, + functionName: "listNFT", + args: [selectedNft.contractAddress, selectedNft.tokenId, parseEther(monthlyPayment), numberOfPayments], + }); + + const listTx = await writeTx(writeListNFT, { blockConfirmations: 1 }); + + console.log("Listing transaction:", listTx); + alert("NFT listed successfully!"); + } catch (error: any) { + console.error("Error:", error); + alert(`Error: ${error.message}`); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedNft, setSelectedNft, monthlyPayment, setMonthlyPayment, numberOfPayments, setNumberOfPayments]); + + return ( +
+

Rent to Own NFTs

+ +
+

Load NFTs from Contract

+
+ setContractAddress(e.target.value)} + className="flex-1 p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + +
+
+ +
+

Your NFTs

+ {nfts.length === 0 ? ( +

No NFTs found. Load your NFTs first.

+ ) : ( +
+ {nfts.map((nft, index) => ( +
+
+ {nft.name} + ID: {nft.tokenId} +
+ +
+ ))} +
+ )} +
+ + {selectedNft && ( +
+

Lend NFT

+
+

+ Selected NFT: {selectedNft.name} +

+

Token ID: {selectedNft.tokenId}

+
+ +
+
+ + setMonthlyPayment(e.target.value)} + className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+
+ + setNumberOfPayments(e.target.value)} + className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+
+ + +
+ )} +
+ ); +}; + +export default RentToOwnPage; diff --git a/packages/nextjs/app/page.tsx b/packages/nextjs/app/page.tsx index ff6af8f..8c8ca6f 100644 --- a/packages/nextjs/app/page.tsx +++ b/packages/nextjs/app/page.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import type { NextPage } from "next"; import { useAccount } from "wagmi"; -import { BugAntIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +import { BanknotesIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; import { Address } from "~~/components/scaffold-eth"; const Home: NextPage = () => { @@ -21,47 +21,20 @@ const Home: NextPage = () => {

Connected Address:

- -

- Get started by editing{" "} - - packages/nextjs/app/page.tsx - -

-

- Edit your smart contract{" "} - - YourContract.sol - {" "} - in{" "} - - packages/hardhat/contracts - -

-
- -

- Tinker with your smart contract using the{" "} - - Debug Contracts - {" "} - tab. -

-
-
- -

- Explore your local transactions with the{" "} - - Block Explorer - {" "} - tab. -

-
+ +
+ Lend +
+ + +
+ Borrow +
+
diff --git a/packages/nextjs/components/dev/SkipTimeComponent.tsx b/packages/nextjs/components/dev/SkipTimeComponent.tsx new file mode 100644 index 0000000..28dd5a0 --- /dev/null +++ b/packages/nextjs/components/dev/SkipTimeComponent.tsx @@ -0,0 +1,66 @@ +import { useState } from "react"; +import { usePublicClient } from "wagmi"; +import { useTargetNetwork } from "~~/hooks/scaffold-eth"; + +interface SkipTimeProps { + reload: () => void; // Callback to reload agreements +} + +export const SkipTimeComponent = ({ reload }: SkipTimeProps) => { + const { targetNetwork } = useTargetNetwork(); + const publicClient = usePublicClient({ chainId: targetNetwork.id }); + const [isLoading, setIsLoading] = useState(false); + const [days, setDays] = useState(30); + + const skipTime = async (days: number) => { + if (!publicClient) { + alert("Public client not available. Ensure you're connected to the correct network."); + return; + } + + setIsLoading(true); + try { + //TODO + // await publicClient.request({ + // method: "evm_increaseTime", + // params: [days * 24 * 60 * 60], + // }); + // await publicClient.request({ + // method: "evm_mine", + // params: [], + // }); + + alert(`Skipped ${days} days!`); + reload(); // Refresh agreements after time skip + } catch (error) { + console.error("Error skipping time:", error); + alert("Failed to skip time. Ensure you're connected to the correct network."); + } finally { + setIsLoading(false); + } + }; + + return ( +
+

Development Tools

+
+ setDays(Number(e.target.value))} + className="border p-2 rounded" + placeholder="Days to skip" + /> + +
+
+ ); +}; diff --git a/packages/nextjs/hooks/useAgreements.ts b/packages/nextjs/hooks/useAgreements.ts new file mode 100644 index 0000000..32a9778 --- /dev/null +++ b/packages/nextjs/hooks/useAgreements.ts @@ -0,0 +1,92 @@ +import { useCallback, useEffect, useState } from "react"; +import { parseEther } from "viem"; +import { useScaffoldContract, useScaffoldReadContract } from "~~/hooks/scaffold-eth"; + +interface Agreement { + id: number; + borrower: string; + lender: string; + nftContract: string; + nftId: string; + monthlyPayment: bigint; + totalPrice: bigint; + totalPaid: bigint; + totalRemaining: bigint; + nextPaymentDue: string; + isActive: boolean; +} + +export function useAgreements() { + const [agreements, setAgreements] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Fetch the total number of agreements + // noinspection TypeScriptValidateTypes + const { + data: agreementCounter, + isLoading: counterLoading, + error: counterError, + } = useScaffoldReadContract({ + contractName: "RentToOwn", + functionName: "agreementCounter", + args: undefined, + }); + + const { data: rentToOwnContract, isLoading: rentToOwnContractLoading } = useScaffoldContract({ + contractName: "RentToOwn", + }); + + const fetchAgreements = useCallback(() => { + if (!agreementCounter || counterLoading || counterError) return; + + const fetchAgreements = async () => { + if (rentToOwnContractLoading || !rentToOwnContract) { + alert("The RentToOwn Contract is loading."); + return []; + } + setLoading(true); + setError(null); + + try { + const loadedAgreements = []; + + for (let i = 0; i < agreementCounter; i++) { + const agreement = await rentToOwnContract.read.agreements(BigInt(i) as any); + console.log(`Agreement ${i}:`, agreement); + const totalPrice = parseEther(agreement[5].toString()); + const totalPaid = parseEther(agreement[6].toString()); + + loadedAgreements.push({ + borrower: agreement[0], // string + lender: agreement[1], // string + nftContract: agreement[2], // string + nftId: agreement[3].toString(), // bigint -> string + monthlyPayment: parseEther(agreement[4].toString()), // bigint -> ether string + nextPaymentDue: new Date(Number(agreement[7]) * 1000).toLocaleDateString(), // bigint -> date string + isActive: agreement[8], // boolean + totalPrice, + totalPaid, + totalRemaining: totalPrice - totalPaid, + id: i, + }); + } + + setAgreements(loadedAgreements); + } catch (err) { + setError(err instanceof Error ? err : new Error("Failed to load agreements")); + } finally { + setLoading(false); + } + }; + + void fetchAgreements(); + }, [agreementCounter, counterLoading, counterError, rentToOwnContract]); + + // Fetch agreements on mount or when agreementCounter changes + useEffect(() => { + void fetchAgreements(); + }, [fetchAgreements]); + + return { agreements, loading, error, reload: fetchAgreements }; +}