diff --git a/frontend/jsconfig.json b/frontend/jsconfig.json deleted file mode 100644 index b8d6842..0000000 --- a/frontend/jsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "compilerOptions": { - "paths": { - "@/*": ["./src/*"] - } - } -} diff --git a/frontend/next-env.d.ts b/frontend/next-env.d.ts new file mode 100644 index 0000000..00a8785 --- /dev/null +++ b/frontend/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. diff --git a/frontend/package.json b/frontend/package.json index c8544d5..29441bd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,41 +12,26 @@ "lint": "next lint" }, "dependencies": { - "@near-wallet-selector/bitte-wallet": "^9.0.2", - "@near-wallet-selector/core": "^9.0.2", - "@near-wallet-selector/ethereum-wallets": "^9.0.2", - "@near-wallet-selector/here-wallet": "^9.0.2", - "@near-wallet-selector/hot-wallet": "^9.0.2", - "@near-wallet-selector/ledger": "^9.0.2", - "@near-wallet-selector/meteor-wallet": "^9.0.2", - "@near-wallet-selector/meteor-wallet-app": "^9.0.2", - "@near-wallet-selector/modal-ui": "^9.0.2", - "@near-wallet-selector/my-near-wallet": "^9.0.2", - "@near-wallet-selector/near-mobile-wallet": "^9.0.2", - "@near-wallet-selector/react-hook": "^9.0.2", - "@near-wallet-selector/sender": "^9.0.2", - "@near-wallet-selector/welldone-wallet": "^9.0.2", - "@reown/appkit": "^1.7.7", - "@reown/appkit-adapter-wagmi": "^1.7.7", - "@wagmi/core": "^2.17.2", + "@hot-labs/near-connect": "^0.6.2", + "@near-js/crypto": "^2.3.1", + "@near-js/providers": "^2.3.1", + "@near-js/transactions": "^2.3.1", + "@near-js/utils": "^2.3.1", + "@walletconnect/sign-client": "^2.21.9", "bootstrap": "^5", "bootstrap-icons": "^1.11.3", + "near-api-js": "^6.3.0", "next": "^15", "react": "^18", - "react-dom": "^18", - "viem": "^2.30.5" - }, - "overrides": { - "@near-wallet-selector/ethereum-wallets": { - "near-api-js": "4.0.3" - } - }, - "resolutions": { - "near-api-js": "4.0.3" + "react-dom": "^18" }, "devDependencies": { + "@types/node": "24.7.2", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.1", "encoding": "^0.1.13", "eslint": "^9.26.0", - "eslint-config-next": "15.3.2" + "eslint-config-next": "15.3.2", + "typescript": "^5.9.3" } } diff --git a/frontend/src/components/DonationBox.jsx b/frontend/src/components/DonationBox.tsx similarity index 63% rename from frontend/src/components/DonationBox.jsx rename to frontend/src/components/DonationBox.tsx index 6bfd0a4..b2efba5 100644 --- a/frontend/src/components/DonationBox.jsx +++ b/frontend/src/components/DonationBox.tsx @@ -1,9 +1,14 @@ +import React from "react"; import DonationForm from "./DonationForm"; -import { useWalletSelector } from '@near-wallet-selector/react-hook'; +import { useNear } from '@/hooks/useNear'; -const DonationBox = ({ setMyDonation }) => { - const { signedAccountId } = useWalletSelector(); +interface DonationBoxProps { + setMyDonation: React.Dispatch>; +} + +export default function DonationBox({ setMyDonation }: DonationBoxProps) { + const { signedAccountId } = useNear(); return (
@@ -23,6 +28,4 @@ const DonationBox = ({ setMyDonation }) => {
); -}; - -export default DonationBox; +} diff --git a/frontend/src/components/DonationForm.jsx b/frontend/src/components/DonationForm.tsx similarity index 51% rename from frontend/src/components/DonationForm.jsx rename to frontend/src/components/DonationForm.tsx index 272c266..c542218 100644 --- a/frontend/src/components/DonationForm.jsx +++ b/frontend/src/components/DonationForm.tsx @@ -1,51 +1,54 @@ import { utils } from "near-api-js"; import { useState } from "react"; -import { useWalletSelector } from '@near-wallet-selector/react-hook'; - +import { useNear } from "@/hooks/useNear"; import { DonationNearContract } from "@/config"; -const DonationForm = ({ setMyDonation }) => { - const { callFunction } = useWalletSelector(); +interface DonationFormProps { + setMyDonation: (amount: number) => void; +} - const [amount, setAmount] = useState(0); +const DonationForm = ({ setMyDonation }: DonationFormProps) => { + const { callFunction } = useNear(); + const [amount, setAmount] = useState(0); - const setDonation = async (amount) => { - let data = await fetch( - "https://api.coingecko.com/api/v3/simple/price?ids=near&vs_currencies=usd", + const setDonation = async (usdAmount: number) => { + const data = await fetch( + "https://api.coingecko.com/api/v3/simple/price?ids=near&vs_currencies=usd" ).then((response) => response.json()); + const near2usd = data["near"]["usd"]; - const amount_in_near = amount / near2usd; - const rounded_two_decimals = Math.round(amount_in_near * 100) / 100; - setAmount(rounded_two_decimals); + const amountInNear = usdAmount / near2usd; + const rounded = Math.round(amountInNear * 100) / 100; + setAmount(rounded); }; - const handleSubmit = async (event) => { + const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); - let deposit = utils.format.parseNearAmount(amount.toString()); +let deposit = utils.format.parseNearAmount(amount.toString()) || "0"; callFunction({ - contractId: DonationNearContract, - method: "donate", - deposit, - }) - .catch(() => { - setMyDonation(-Number(amount)); - }); - + contractId: DonationNearContract, + method: "donate", + deposit, + }).catch(() => { + setMyDonation(-amount); + }); + setMyDonation(amount); }; return ( <>
- {[10, 20, 50, 100].map((amount) => ( -
+ {[10, 20, 50, 100].map((usdAmount) => ( +
))} @@ -60,9 +63,9 @@ const DonationForm = ({ setMyDonation }) => { id="donation" value={amount} type="number" - min="0" - step="0.01" - onChange={(e) => setAmount(e.target.value)} + min={0} + step={0.01} + onChange={(e) => setAmount(parseFloat(e.target.value))} className="form-control" /> diff --git a/frontend/src/components/DonationsTable.jsx b/frontend/src/components/DonationsTable.tsx similarity index 71% rename from frontend/src/components/DonationsTable.jsx rename to frontend/src/components/DonationsTable.tsx index be02622..44b1900 100644 --- a/frontend/src/components/DonationsTable.jsx +++ b/frontend/src/components/DonationsTable.tsx @@ -1,39 +1,45 @@ import { utils } from "near-api-js"; import { useEffect, useState } from "react"; -import { useWalletSelector } from '@near-wallet-selector/react-hook'; +import { useNear } from "@/hooks/useNear"; import { DonationNearContract } from "@/config"; +interface Donation { + account_id: string; + total_amount: string; +} -const DonationsTable = () => { - const { signedAccountId, viewFunction } = useWalletSelector(); - const [donations, setDonations] = useState([]); +export default function DonationsTable() { + const { signedAccountId, viewFunction } = useNear(); + const [donations, setDonations] = useState([]); const [currentPage, setCurrentPage] = useState(1); const [lastPage, setLastPage] = useState(0); const donationsPerPage = 5; - const getDonations = async (page) => { - const number_of_donors = await viewFunction({ + const getDonations = async (page: number): Promise => { + const number_of_donors = (await viewFunction({ contractId: DonationNearContract, method: "number_of_donors", - }); + })) as number; setLastPage(Math.ceil(number_of_donors / donationsPerPage)); const fromIndex = (page - 1) * donationsPerPage; - const donations = await viewFunction({ + + const donations = (await viewFunction({ contractId: DonationNearContract, method: "get_donations", args: { from_index: fromIndex.toString(), limit: donationsPerPage.toString(), }, - }); + })) as Donation[]; + return donations; }; useEffect(() => { if (!signedAccountId) return; getDonations(currentPage).then((loadedDonations) => - setDonations(loadedDonations), + setDonations(loadedDonations) ); }, [signedAccountId, currentPage]); @@ -65,7 +71,9 @@ const DonationsTable = () => {
Page {currentPage}
); -}; - -export default DonationsTable; +} diff --git a/frontend/src/components/MyDonation.jsx b/frontend/src/components/MyDonation.tsx similarity index 54% rename from frontend/src/components/MyDonation.jsx rename to frontend/src/components/MyDonation.tsx index 3739e62..4568114 100644 --- a/frontend/src/components/MyDonation.jsx +++ b/frontend/src/components/MyDonation.tsx @@ -1,35 +1,47 @@ -import {useEffect, useState } from "react"; -import { useWalletSelector } from '@near-wallet-selector/react-hook'; +import { useEffect, useState } from "react"; +import { useNear } from "@/hooks/useNear"; import { DonationNearContract } from "@/config"; import { utils } from "near-api-js"; -const MyDonation = ({ myDonation }) => { - const { signedAccountId, viewFunction } = useWalletSelector(); - const [donation, setDonation] = useState(0); +interface MyDonationProps { + myDonation?: number; +} + +interface DonationResponse { + total_amount: string; +} + +const MyDonation = ({ myDonation }: MyDonationProps) => { + const { signedAccountId, viewFunction } = useNear(); + const [donation, setDonation] = useState(0); useEffect(() => { if (!myDonation) return; - - setDonation( - Math.round((Number(donation) + Number(myDonation)) * 100) / 100, + setDonation((prev) => + Math.round((prev + Number(myDonation)) * 100) / 100 ); }, [myDonation]); useEffect(() => { if (!signedAccountId) return; + const getMyDonations = async () => { if (signedAccountId.trim() === "") return; console.log("Getting donations for account: ", signedAccountId); - const loadedDonation = await viewFunction({ + + const loadedDonation = (await viewFunction({ contractId: DonationNearContract, method: "get_donation_for_account", - args: { - account_id: signedAccountId, - }, - }); + args: { account_id: signedAccountId }, + })) as DonationResponse; + + const formatted = parseFloat( + utils.format.formatNearAmount(loadedDonation.total_amount) + ); - setDonation(utils.format.formatNearAmount(loadedDonation.total_amount)); + setDonation(formatted); }; + getMyDonations(); }, [signedAccountId]); diff --git a/frontend/src/components/Navigation.jsx b/frontend/src/components/Navigation.jsx deleted file mode 100644 index 7be5991..0000000 --- a/frontend/src/components/Navigation.jsx +++ /dev/null @@ -1,45 +0,0 @@ -import Image from "next/image"; -import Link from "next/link"; -import { useEffect, useState } from "react"; -import NearLogo from "/public/near-logo.svg"; -import { useWalletSelector } from '@near-wallet-selector/react-hook'; - -export const Navigation = () => { - const { signedAccountId, signIn, signOut } = useWalletSelector(); - const [action, setAction] = useState(() => {}); - const [label, setLabel] = useState("Loading..."); - - useEffect(() => { - - if (signedAccountId) { - setAction(() => signOut); - setLabel(`Logout ${signedAccountId}`); - } else { - setAction(() => signIn); - setLabel("Login"); - } - }, [signedAccountId]); - - return ( - - ); -}; diff --git a/frontend/src/components/Navigation.tsx b/frontend/src/components/Navigation.tsx new file mode 100644 index 0000000..f6bb5fe --- /dev/null +++ b/frontend/src/components/Navigation.tsx @@ -0,0 +1,44 @@ +import Image from "next/image"; +import Link from "next/link"; +import { useNear } from "@/hooks/useNear"; +import NearLogo from "/public/near-logo.svg"; + +export const Navigation = () => { + const { signedAccountId, loading, signIn, signOut } = useNear(); + + const handleAction = () => { + if (signedAccountId) { + signOut(); + } else { + signIn(); + } + }; + + const label = loading + ? "Loading..." + : signedAccountId + ? `Logout ${signedAccountId}` + : "Login"; + + return ( + + ); +}; diff --git a/frontend/src/config.js b/frontend/src/config.js deleted file mode 100644 index 7fa2cb6..0000000 --- a/frontend/src/config.js +++ /dev/null @@ -1,24 +0,0 @@ -const contractPerNetwork = { - testnet: "donation.near-examples.testnet", -}; - -export const NetworkId = "testnet"; -export const DonationNearContract = contractPerNetwork[NetworkId]; - -// Chains for EVM Wallets -const evmWalletChains = { - mainnet: { - chainId: 397, - name: "Near Mainnet", - explorer: "https://eth-explorer.near.org", - rpc: "https://eth-rpc.mainnet.near.org", - }, - testnet: { - chainId: 398, - name: "Near Testnet", - explorer: "https://eth-explorer-testnet.near.org", - rpc: "https://eth-rpc.testnet.near.org", - }, -}; - -export const EVMWalletChain = evmWalletChains[NetworkId]; diff --git a/frontend/src/config.ts b/frontend/src/config.ts new file mode 100644 index 0000000..56ac066 --- /dev/null +++ b/frontend/src/config.ts @@ -0,0 +1,42 @@ +// src/config.ts + +// Define the contract per network +const contractPerNetwork: Record = { + testnet: "donation.near-examples.testnet", + mainnet: "donation.near-examples.near", +}; + +// Define network type (matches WalletSelector Network type) +export type NearNetwork = "testnet" | "mainnet"; + +// Set current network +export const NetworkId: NearNetwork = "testnet"; + +// Get the contract for the current network +export const DonationNearContract: string = contractPerNetwork[NetworkId]; + +// Chains for EVM Wallets +interface EVMWalletChain { + chainId: number; + name: string; + explorer: string; + rpc: string; +} + +const evmWalletChains: Record = { + mainnet: { + chainId: 397, + name: "Near Mainnet", + explorer: "https://eth-explorer.near.org", + rpc: "https://eth-rpc.mainnet.near.org", + }, + testnet: { + chainId: 398, + name: "Near Testnet", + explorer: "https://eth-explorer-testnet.near.org", + rpc: "https://eth-rpc.testnet.near.org", + }, +}; + +// Export the current EVM wallet chain +export const EVMWalletChain: EVMWalletChain = evmWalletChains[NetworkId]; diff --git a/frontend/src/global.d.ts b/frontend/src/global.d.ts new file mode 100644 index 0000000..36c7926 --- /dev/null +++ b/frontend/src/global.d.ts @@ -0,0 +1,6 @@ +declare module '*.module.css' { + const classes: { [key: string]: string }; + export default classes; +} + +declare module '*.css'; // optional: for normal CSS imports diff --git a/frontend/src/hooks/useNear.tsx b/frontend/src/hooks/useNear.tsx new file mode 100644 index 0000000..7ff052f --- /dev/null +++ b/frontend/src/hooks/useNear.tsx @@ -0,0 +1,129 @@ +import { useEffect, useState } from "react"; +import { JsonRpcProvider } from "@near-js/providers"; +import type { NearConnector, NearWalletBase } from "@hot-labs/near-connect"; + +interface ViewFunctionParams { + contractId: string; + method: string; + args?: Record; +} + +interface FunctionCallParams { + contractId: string; + method: string; + args?: Record; + gas?: string; + deposit?: string; +} + +let connector: NearConnector | undefined; +const provider = new JsonRpcProvider({ url: "https://test.rpc.fastnear.com" }); + +export function useNear() { + const [wallet, setWallet] = useState(undefined); + const [signedAccountId, setSignedAccountId] = useState(""); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (typeof window === "undefined") return; + + async function initializeConnector() { + if (!connector) { + const { NearConnector } = await import("@hot-labs/near-connect"); + connector = new NearConnector({ network: "testnet" }); + } + + async function reload() { + try { + const { wallet, accounts } = await connector!.getConnectedWallet(); + setWallet(wallet); + setSignedAccountId(accounts[0].accountId); + } catch { + setWallet(undefined); + setSignedAccountId(""); + } finally { + setLoading(false); + } + } + + async function onSignOut() { + setWallet(undefined); + setSignedAccountId(""); + } + + async function onSignIn(payload: { wallet: NearWalletBase }) { + console.log("Signed in with payload", payload); + setWallet(payload.wallet); + const accounts = await payload.wallet.getAccounts(); + setSignedAccountId(accounts[0]?.accountId || ""); + } + + connector.on("wallet:signOut", onSignOut); + connector.on("wallet:signIn", onSignIn); + + await reload(); + + return () => { + if (!connector) return; + connector.off("wallet:signOut", onSignOut); + connector.off("wallet:signIn", onSignIn); + }; + } + + initializeConnector(); + }, []); + + async function signIn() { + if (!connector) return; + const wallet = await connector.connect(); + console.log("Connected wallet", wallet); + } + + async function signOut() { + if (!connector || !wallet) return; + await connector.disconnect(wallet); + console.log("Disonnected wallet"); + } + + async function viewFunction({ contractId, method, args = {} }: ViewFunctionParams) { + const response = await provider.query({ + request_type: "call_function", + account_id: contractId, + method_name: method, + args_base64: Buffer.from(JSON.stringify(args)).toString("base64"), + finality: "final", + }); + // @ts-ignore - response type from provider + return JSON.parse(Buffer.from(response.result).toString()); + } + + async function callFunction({ contractId, method, args = {}, gas = "30000000000000", deposit = "0" }: FunctionCallParams) { + if (!wallet) throw new Error("Wallet not connected"); + + return wallet.signAndSendTransaction({ + receiverId: contractId, + actions: [ + { + type: "FunctionCall", + params: { + methodName: method, + args, + gas, + deposit, + }, + }, + ], + }); + } + + return { + signedAccountId, + wallet, + signIn, + signOut, + loading, + viewFunction, + callFunction, + provider, + }; +} \ No newline at end of file diff --git a/frontend/src/pages/_app.js b/frontend/src/pages/_app.js deleted file mode 100644 index 4dbb461..0000000 --- a/frontend/src/pages/_app.js +++ /dev/null @@ -1,46 +0,0 @@ -import { wagmiAdapter, web3Modal } from '@/wallets/web3modal'; -import { Navigation } from "@/components/Navigation"; -import { DonationNearContract, NetworkId } from "@/config"; -import "@/styles/globals.css"; -import '@near-wallet-selector/modal-ui/styles.css'; - -import { setupMyNearWallet } from '@near-wallet-selector/my-near-wallet'; -import { setupMeteorWallet } from '@near-wallet-selector/meteor-wallet'; -import { setupMeteorWalletApp } from '@near-wallet-selector/meteor-wallet-app'; -import { setupBitteWallet } from '@near-wallet-selector/bitte-wallet'; -import { setupEthereumWallets } from '@near-wallet-selector/ethereum-wallets'; -import { setupHotWallet } from '@near-wallet-selector/hot-wallet'; -import { setupLedger } from '@near-wallet-selector/ledger'; -import { setupSender } from '@near-wallet-selector/sender'; -import { setupHereWallet } from '@near-wallet-selector/here-wallet'; -import { setupNearMobileWallet } from '@near-wallet-selector/near-mobile-wallet'; -import { setupWelldoneWallet } from '@near-wallet-selector/welldone-wallet'; -import { WalletSelectorProvider } from '@near-wallet-selector/react-hook'; - -const walletSelectorConfig = { - network: NetworkId, - createAccessKeyFor: DonationNearContract, - modules: [ - setupEthereumWallets({ wagmiConfig: wagmiAdapter.wagmiConfig, web3Modal }), - setupBitteWallet(), - setupMeteorWallet(), - setupMeteorWalletApp({contractId: DonationNearContract}), - setupHotWallet(), - setupLedger(), - setupSender(), - setupHereWallet(), - setupNearMobileWallet(), - setupWelldoneWallet(), - setupMyNearWallet(), - ], -} - -export default function App({ Component, pageProps }) { - - return ( - - - - - ); -} diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx new file mode 100644 index 0000000..f788c20 --- /dev/null +++ b/frontend/src/pages/_app.tsx @@ -0,0 +1,13 @@ +import "@/styles/globals.css"; + +import type { AppProps } from "next/app"; +import { Navigation } from "@/components/Navigation"; + +export default function App({ Component, pageProps }: AppProps) { + return ( + <> + + + + ); +} \ No newline at end of file diff --git a/frontend/src/pages/index.js b/frontend/src/pages/index.tsx similarity index 99% rename from frontend/src/pages/index.js rename to frontend/src/pages/index.tsx index 7706c32..f99c479 100644 --- a/frontend/src/pages/index.js +++ b/frontend/src/pages/index.tsx @@ -1,19 +1,22 @@ +import { useState } from "react"; import DonationBox from "@/components/DonationBox"; import DonationsTable from "@/components/DonationsTable"; import MyDonation from "@/components/MyDonation"; -import { useState } from "react"; export default function Home() { const [myDonation, setMyDonation] = useState(0); + return (

My Donation

+

Latest Donations

+
diff --git a/frontend/src/wallets/web3modal.js b/frontend/src/wallets/web3modal.js deleted file mode 100644 index 8f72989..0000000 --- a/frontend/src/wallets/web3modal.js +++ /dev/null @@ -1,54 +0,0 @@ -import { createAppKit } from "@reown/appkit/react"; -import { reconnect } from "@wagmi/core"; -import { nearTestnet } from "@reown/appkit/networks"; -import { WagmiAdapter } from "@reown/appkit-adapter-wagmi"; - -// Get your projectId at https://cloud.reown.com -const projectId = "5bb0fe33763b3bea40b8d69e4269b4ae"; - -const connectors = [ - walletConnect({ - projectId, - metadata: { - name: "Donation", - description: "Examples demonstrating integrations with NEAR blockchain", - url: "https://near.github.io/wallet-selector", - icons: ["https://near.github.io/wallet-selector/favicon.ico"], - }, - showQrModal: false, // showQrModal must be false - }), - injected({ shimDisconnect: true }), -]; - -export const wagmiAdapter = new WagmiAdapter({ - projectId, - connectors, - networks: [nearTestnet], -}); - -// Preserve login state on page reload -reconnect(wagmiAdapter.wagmiConfig); - -// Modal for login -export const web3Modal = createAppKit({ - adapters: [wagmiAdapter], - projectId, - networks: [nearTestnet], - enableWalletConnect: true, - features: { - analytics: true, - swaps: false, - onramp: false, - email: false, // Smart accounts (Safe contract) not available on NEAR Protocol, only EOA. - socials: false, // Smart accounts (Safe contract) not available on NEAR Protocol, only EOA. - }, - coinbasePreference: "eoaOnly", // Smart accounts (Safe contract) not available on NEAR Protocol, only EOA. - allWallets: "SHOW", -}); - -// force reconnecting if the user has already signed in with an ethereum wallet -// this is a workaround until `ethereum-wallets` supports the `reconnect` method -if (typeof window !== "undefined") { - const recentWallets = localStorage.getItem("near-wallet-selector:recentlySignedInWallets"); - recentWallets && recentWallets.includes("ethereum-wallets") && reconnect(wagmiAdapter.wagmiConfig) -} \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..41583c8 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": [ + "./src/*" + ] + }, + "jsx": "preserve", + "strict": true, + "moduleResolution": "node", + "skipLibCheck": true, + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "noEmit": true, + "incremental": true, + "module": "esnext", + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true + }, + "include": [ + "**/*.ts", + "**/*.tsx", + "global.d.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file