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 61c2073..ad2ad95 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,34 +12,26 @@ "lint": "next lint" }, "dependencies": { - "@near-js/providers": "^1.0.1", - "@near-wallet-selector/bitte-wallet": "^8.10.0", - "@near-wallet-selector/core": "^8.10.0", - "@near-wallet-selector/ethereum-wallets": "^8.10.0", - "@near-wallet-selector/here-wallet": "^8.10.0", - "@near-wallet-selector/hot-wallet": "^8.10.0", - "@near-wallet-selector/ledger": "^8.10.0", - "@near-wallet-selector/meteor-wallet": "^8.10.0", - "@near-wallet-selector/meteor-wallet-app": "^8.10.0", - "@near-wallet-selector/modal-ui": "^8.10.0", - "@near-wallet-selector/my-near-wallet": "^8.10.0", - "@near-wallet-selector/near-mobile-wallet": "^8.10.0", - "@near-wallet-selector/nightly": "^8.10.0", - "@near-wallet-selector/react-hook": "^8.9.15", - "@near-wallet-selector/sender": "^8.10.0", - "@near-wallet-selector/welldone-wallet": "^8.10.0", - "@reown/appkit": "^1.6.9", - "@reown/appkit-adapter-wagmi": "^1.6.9", + "@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": "^5.0.1", - "next": "15.2.1", + "near-api-js": "^6.3.0", + "next": "^15", "react": "^18", "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.16.0", - "eslint-config-next": "15.0.3" + "eslint-config-next": "15.0.3", + "typescript": "5.9.3" } } diff --git a/frontend/src/components/Navigation.jsx b/frontend/src/components/Navigation.jsx deleted file mode 100644 index 1efc3d5..0000000 --- a/frontend/src/components/Navigation.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import Image from 'next/image'; -import Link from 'next/link'; -import { useEffect, useState } from 'react'; -import { useWalletSelector } from '@near-wallet-selector/react-hook'; - -import NearLogo from '/public/near-logo.svg'; - -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 ( - - ); -}; \ No newline at end of file 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.ts similarity index 100% rename from frontend/src/config.js rename to frontend/src/config.ts diff --git a/frontend/src/global.d.ts b/frontend/src/global.d.ts new file mode 100644 index 0000000..14c22bb --- /dev/null +++ b/frontend/src/global.d.ts @@ -0,0 +1,6 @@ +declare module "*.png" { + const value: string; + export default value; + +} +declare module "*.module.css"; 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 4280920..0000000 --- a/frontend/src/pages/_app.js +++ /dev/null @@ -1,46 +0,0 @@ -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'; -import { wagmiAdapter, web3Modal } from '@/wallets/web3modal'; -import { Navigation } from '@/components/Navigation'; -import { NetworkId, CounterContract } from '@/config'; - -const walletSelectorConfig = { - network: NetworkId, - createAccessKeyFor: CounterContract, - modules: [ - setupEthereumWallets({ wagmiConfig: wagmiAdapter.wagmiConfig, web3Modal }), - setupBitteWallet(), - setupMeteorWallet(), - setupMeteorWalletApp({contractId: CounterContract}), - setupHotWallet(), - setupLedger(), - setupSender(), - setupHereWallet(), - setupNearMobileWallet(), - setupWelldoneWallet(), - setupMyNearWallet(), - ], -} - -export default function App({ Component, pageProps }) { - - return ( - - - - - ); -} \ No newline at end of file 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.js deleted file mode 100644 index 387b599..0000000 --- a/frontend/src/pages/index.js +++ /dev/null @@ -1,140 +0,0 @@ -import { useEffect, useState } from 'react'; - -import styles from '@/styles/app.module.css'; -import { useWalletSelector } from '@near-wallet-selector/react-hook'; -import { CounterContract } from '@/config'; - - -export default function Home() { - const { signedAccountId, callFunction, viewFunction } = useWalletSelector(); - const [number, setNumber] = useState(0); - const [numberIncrement, setNumberIncrement] = useState(0); - - const [leftEyeVisible, setLeftEyeVisible] = useState(true); - const [rightEyeVisible, setRightEyeVisible] = useState(true); - const [tongueVisible, setTongueVisible] = useState(false); - const [dotOn, setDotOn] = useState(true); - - const [globalInterval, setGlobalInterval] = useState(null); - - useEffect(() => { - fetchNumber(); - - // Fetch the number every two seconds - let interval = setInterval(fetchNumber, 1500); - setGlobalInterval(interval); - - return () => clearInterval(interval); - }, []) - - useEffect(() => { - // interrupt the constant fetching of the number - clearInterval(globalInterval); - - // Debounce the increment call until the user stops clicking - const getData = setTimeout(() => { - if (numberIncrement === 0) return; - - setNumberIncrement(0); - - // Try to increment the counter, fetch the number afterwords - callFunction({ contractId: CounterContract, method: 'increment', args: { number: numberIncrement } }) - .finally(() => { - fetchNumber(); - let interval = setInterval(fetchNumber, 1500) - setGlobalInterval(interval); - }) - - }, 500) - - return () => clearTimeout(getData); - }, [numberIncrement]) - - const fetchNumber = async () => { - setDotOn(true); - console.log("fetching number") - const num = await viewFunction({ contractId: CounterContract, method: "get_num" }); - setNumber(num); - setDotOn(false); - } - - const call = (method) => async () => { - const methodToState = { - increment: () => { - setNumberIncrement(numberIncrement + 1) - setNumber(number + 1) - }, - decrement: () => { - setNumberIncrement(numberIncrement - 1) - setNumber(number - 1) - }, - reset: async () => { - setNumberIncrement(0) - setNumber(0) - callFunction({ contractId: CounterContract, method: 'reset' }).then(async () => { - await fetchNumber(); - }) - }, - } - - methodToState[method]?.(); - } - - return ( -
-

This global counter lives in the NEAR blockchain!

- {!signedAccountId &&
-

You'll need to sign in to interact with the counter:

-
} -
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
= 0 ? 'smile' : 'cry'}`}>
-
-
-
-
{number}
-
-
-
- - -
-
-
- - - - -
-
-
-
-
- -
-
- ); -} \ No newline at end of file diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx new file mode 100644 index 0000000..6e103ac --- /dev/null +++ b/frontend/src/pages/index.tsx @@ -0,0 +1,166 @@ +import { useEffect, useState } from 'react'; + +import styles from '@/styles/app.module.css'; +import { useNear } from '@/hooks/useNear'; +import { CounterContract } from '@/config'; + +export default function Home() { + const { signedAccountId, callFunction, viewFunction } = useNear(); + + const [number, setNumber] = useState(0); + const [numberIncrement, setNumberIncrement] = useState(0); + + const [leftEyeVisible, setLeftEyeVisible] = useState(true); + const [rightEyeVisible, setRightEyeVisible] = useState(true); + const [tongueVisible, setTongueVisible] = useState(false); + const [dotOn, setDotOn] = useState(true); + + const [globalInterval, setGlobalInterval] = useState | null>(null); + + // Fetch number initially and set interval + useEffect(() => { + fetchNumber(); + + const interval = setInterval(fetchNumber, 1500); + setGlobalInterval(interval); + + return () => { + if (interval) clearInterval(interval); + }; + }, []); + + // Handle debounced increment + useEffect(() => { + if (globalInterval) clearInterval(globalInterval); + + const timeout = setTimeout(() => { + if (numberIncrement === 0) return; + + const incrementValue = numberIncrement; + setNumberIncrement(0); + + callFunction({ + contractId: CounterContract, + method: 'increment', + args: { number: incrementValue }, + }).finally(() => { + fetchNumber(); + const interval = setInterval(fetchNumber, 1500); + setGlobalInterval(interval); + }); + }, 500); + + return () => clearTimeout(timeout); + }, [numberIncrement]); + + const fetchNumber = async () => { + setDotOn(true); + const num = await viewFunction({ contractId: CounterContract, method: 'get_num' }); + setNumber(num as number); + setDotOn(false); + }; + + type Method = 'increment' | 'decrement' | 'reset'; + const call = (method: Method) => async () => { + switch (method) { + case 'increment': + setNumberIncrement(prev => prev + 1); + setNumber(prev => prev + 1); + break; + case 'decrement': + setNumberIncrement(prev => prev - 1); + setNumber(prev => prev - 1); + break; + case 'reset': + setNumberIncrement(0); + setNumber(0); + await callFunction({ contractId: CounterContract, method: 'reset' }); + await fetchNumber(); + break; + } + }; + + return ( +
+

This global counter lives in the NEAR blockchain!

+ + {!signedAccountId && ( +
+

You'll need to sign in to interact with the counter:

+
+ )} + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
= 0 ? 'smile' : 'cry'}`}>
+
+
+
+
{number}
+
+ +
+
+ + +
+ +
+
+ + + + +
+
+
+ +
+
+
+
+ ); +} diff --git a/frontend/src/wallets/web3modal.js b/frontend/src/wallets/web3modal.js deleted file mode 100644 index 55f4ba0..0000000 --- a/frontend/src/wallets/web3modal.js +++ /dev/null @@ -1,46 +0,0 @@ -import { injected,walletConnect } from '@wagmi/connectors'; -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: "Counters", - 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], -}); - -reconnect(wagmiAdapter.wagmiConfig); - -export const web3Modal = createAppKit({ - adapters: [wagmiAdapter], - projectId, - networks: [nearTestnet], - defaultNetwork: 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. -}); diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..2b628ff --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "baseUrl": "src", + "paths": { + "@/*": ["*"] + }, + "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"] +}