From f5889c1a380278c7532e2f75fbea67467b54bfd2 Mon Sep 17 00:00:00 2001
From: Decebal Dobrica <352761+decebal@users.noreply.github.com>
Date: Sat, 16 Nov 2024 15:40:56 +0000
Subject: [PATCH] chore: fix types
---
packages/nextjs/app/borrow/page.tsx | 133 ++++++++++
packages/nextjs/app/lend/page.tsx | 230 ++++++++++++++++++
packages/nextjs/app/page.tsx | 49 +---
.../components/dev/SkipTimeComponent.tsx | 66 +++++
packages/nextjs/hooks/useAgreements.ts | 92 +++++++
5 files changed, 532 insertions(+), 38 deletions(-)
create mode 100644 packages/nextjs/app/borrow/page.tsx
create mode 100644 packages/nextjs/app/lend/page.tsx
create mode 100644 packages/nextjs/components/dev/SkipTimeComponent.tsx
create mode 100644 packages/nextjs/hooks/useAgreements.ts
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}
+
+
+
+
+
+
+ )}
+
+ );
+};
+
+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 };
+}