diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index bffb357..38e898a 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -1,3 +1,10 @@ { - "extends": "next/core-web-vitals" + "extends": [ + "next/core-web-vitals" // extended set of recommended rules from Next.js + ], + "plugins": ["simple-import-sort"], + "root": true, + "rules": { + "simple-import-sort/imports": "warn" + } } diff --git a/frontend/next.config.js b/frontend/next.config.js index 25f7984..91ef62f 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -1,14 +1,6 @@ -const isProduction = process.env.NODE_ENV === 'production' - /** @type {import('next').NextConfig} */ const nextConfig = { - images: { - unoptimized: true, - }, - basePath: isProduction ? '/hello-near-examples' : '', - output: "export", - distDir: 'build', reactStrictMode: true, -} +}; -module.exports = nextConfig; \ No newline at end of file +module.exports = nextConfig; diff --git a/frontend/package.json b/frontend/package.json index 0008e24..cddf082 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,25 +16,33 @@ "@near-wallet-selector/bitte-wallet": "^8.9.13", "@near-wallet-selector/core": "^8.9.13", "@near-wallet-selector/ethereum-wallets": "^8.9.13", + "@near-wallet-selector/here-wallet": "^8.9.13", + "@near-wallet-selector/ledger": "^8.9.13", "@near-wallet-selector/meteor-wallet": "^8.9.13", "@near-wallet-selector/modal-ui": "^8.9.13", "@near-wallet-selector/my-near-wallet": "^8.9.13", - "@web3modal/wagmi": "^5.1.9", + "@near-wallet-selector/sender": "^8.9.13", + "@web3modal/wagmi": "^5.1.10", "bootstrap": "^5", "bootstrap-icons": "^1.11.3", - "near-api-js": "^5", + "near-api-js": "^4.0.3", "next": "14.2.13", "react": "^18", "react-dom": "^18", - "wagmi": "^2.12.14" + "wagmi": "^2.12.16" }, - "resolutions": { - "near-api-js": "4.0.3" + "overrides": { + "@near-wallet-selector/ethereum-wallets": { + "near-api-js": "4.0.3" + } }, "devDependencies": { + "@babel/preset-env": "^7.25.4", + "@babel/preset-react": "^7.24.7", "encoding": "^0.1.13", - "eslint": "^9", + "eslint": "^8.57.1", "eslint-config-next": "14.2.13", + "eslint-plugin-simple-import-sort": "^12.1.1", "pino-pretty": "^11.2.2" } } diff --git a/frontend/src/components/cards.js b/frontend/src/components/cards.js index 604a493..ad5307c 100644 --- a/frontend/src/components/cards.js +++ b/frontend/src/components/cards.js @@ -8,7 +8,7 @@ export const Cards = () => {

@@ -17,11 +17,7 @@ export const Cards = () => {

Learn how this application works, and what you can build on Near.

- +

Near Integration ->

@@ -29,4 +25,4 @@ export const Cards = () => { ); -}; \ No newline at end of file +}; diff --git a/frontend/src/components/navigation.js b/frontend/src/components/navigation.js index 6849b8b..101f9f0 100644 --- a/frontend/src/components/navigation.js +++ b/frontend/src/components/navigation.js @@ -1,13 +1,13 @@ import Image from 'next/image'; import Link from 'next/link'; -import { useEffect, useState, useContext } from 'react'; +import { useContext,useEffect, useState } from 'react'; -import { NearContext } from '@/context'; import NearLogo from '/public/near-logo.svg'; +import { NearContext } from '@/wallets/near'; export const Navigation = () => { const { signedAccountId, wallet } = useContext(NearContext); - const [action, setAction] = useState(() => { }); + const [action, setAction] = useState(() => {}); const [label, setLabel] = useState('Loading...'); useEffect(() => { @@ -28,10 +28,12 @@ export const Navigation = () => { NEAR -
- +
+
); -}; \ No newline at end of file +}; diff --git a/frontend/src/config.js b/frontend/src/config.js index eb0ffd9..bbdaaf5 100644 --- a/frontend/src/config.js +++ b/frontend/src/config.js @@ -3,22 +3,22 @@ const contractPerNetwork = { testnet: 'hello.near-examples.testnet', }; -// Chains for EVM Wallets +// 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", + 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", + name: 'Near Testnet', + explorer: 'https://eth-explorer-testnet.near.org', + rpc: 'https://eth-rpc.testnet.near.org', }, -} +}; export const NetworkId = 'testnet'; export const HelloNearContract = contractPerNetwork[NetworkId]; -export const EVMWalletChain = evmWalletChains[NetworkId]; \ No newline at end of file +export const EVMWalletChain = evmWalletChains[NetworkId]; diff --git a/frontend/src/context.js b/frontend/src/context.js deleted file mode 100644 index 7422309..0000000 --- a/frontend/src/context.js +++ /dev/null @@ -1,13 +0,0 @@ -import { createContext } from 'react'; - -/** - * @typedef NearContext - * @property {import('./wallets/near').Wallet} wallet Current wallet - * @property {string} signedAccountId The AccountId of the signed user - */ - -/** @type {import ('react').Context} */ -export const NearContext = createContext({ - wallet: undefined, - signedAccountId: '' -}); \ No newline at end of file diff --git a/frontend/src/pages/_app.js b/frontend/src/pages/_app.js index 6294258..93be4b1 100644 --- a/frontend/src/pages/_app.js +++ b/frontend/src/pages/_app.js @@ -1,18 +1,19 @@ +import '@/styles/globals.css'; + import { useEffect, useState } from 'react'; -import '@/styles/globals.css'; -import { NearContext } from '@/context'; import { Navigation } from '@/components/navigation'; - -import { Wallet } from '@/wallets/near'; import { NetworkId } from '@/config'; +import { NearContext,Wallet } from '@/wallets/near'; const wallet = new Wallet({ networkId: NetworkId }); export default function MyApp({ Component, pageProps }) { const [signedAccountId, setSignedAccountId] = useState(''); - useEffect(() => { wallet.startUp(setSignedAccountId) }, []); + useEffect(() => { + wallet.startUp(setSignedAccountId); + }, []); return ( diff --git a/frontend/src/pages/hello-near/index.js b/frontend/src/pages/hello-near/index.js index 055d42e..54c19b4 100644 --- a/frontend/src/pages/hello-near/index.js +++ b/frontend/src/pages/hello-near/index.js @@ -1,9 +1,10 @@ -import { useState, useEffect, useContext } from 'react'; +import { useContext,useEffect, useState } from 'react'; -import { NearContext } from '@/context'; +import { Cards } from '@/components/cards'; import styles from '@/styles/app.module.css'; +import { NearContext } from '@/wallets/near'; + import { HelloNearContract } from '../../config'; -import { Cards } from '@/components/cards'; // Contract that the app will interact with const CONTRACT = HelloNearContract; @@ -19,9 +20,7 @@ export default function HelloNear() { useEffect(() => { if (!wallet) return; - wallet.viewMethod({ contractId: CONTRACT, method: 'get_greeting' }).then( - greeting => setGreeting(greeting) - ); + wallet.viewMethod({ contractId: CONTRACT, method: 'get_greeting' }).then((greeting) => setGreeting(greeting)); }, [wallet]); useEffect(() => { @@ -30,11 +29,9 @@ export default function HelloNear() { const saveGreeting = async () => { setShowSpinner(true); - try { - await wallet.callMethod({ contractId: CONTRACT, method: 'set_greeting', args: { greeting: newGreeting } }); - const greeting = await wallet.viewMethod({ contractId: CONTRACT, method: 'get_greeting' }); - setGreeting(greeting); - } catch (e) { console.error(e); } + await wallet.callMethod({ contractId: CONTRACT, method: 'set_greeting', args: { greeting: newGreeting } }); + const greeting = await wallet.viewMethod({ contractId: CONTRACT, method: 'get_greeting' }); + setGreeting(greeting); setShowSpinner(false); }; @@ -56,15 +53,12 @@ export default function HelloNear() { type="text" className="form-control w-20" placeholder="Store a new greeting" - onChange={t => setNewGreeting(t.target.value)} + onChange={(t) => setNewGreeting(t.target.value)} />
@@ -75,4 +69,4 @@ export default function HelloNear() { ); -} \ No newline at end of file +} diff --git a/frontend/src/pages/index.js b/frontend/src/pages/index.js index b90e0cc..acb1990 100644 --- a/frontend/src/pages/index.js +++ b/frontend/src/pages/index.js @@ -2,8 +2,8 @@ import Image from 'next/image'; import NearLogo from '/public/near.svg'; import NextLogo from '/public/next.svg'; -import styles from '@/styles/app.module.css'; import { Cards } from '@/components/cards'; +import styles from '@/styles/app.module.css'; export default function Home() { return ( @@ -11,21 +11,14 @@ export default function Home() {
- NEAR Logo + NEAR Logo

+

Next.js Logo
@@ -35,4 +28,4 @@ export default function Home() { ); -} \ No newline at end of file +} diff --git a/frontend/src/styles/app.module.css b/frontend/src/styles/app.module.css index 85e5c04..be4a369 100644 --- a/frontend/src/styles/app.module.css +++ b/frontend/src/styles/app.module.css @@ -51,7 +51,9 @@ border-radius: var(--border-radius); background: rgba(var(--card-rgb), 0); border: 1px solid rgba(var(--card-border-rgb), 0); - transition: background 200ms, border 200ms; + transition: + background 200ms, + border 200ms; } .card span { @@ -180,9 +182,7 @@ border-radius: 0; border: none; border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); - background: linear-gradient(to bottom, - rgba(var(--background-start-rgb), 1), - rgba(var(--callout-rgb), 0.5)); + background: linear-gradient(to bottom, rgba(var(--background-start-rgb), 1), rgba(var(--callout-rgb), 0.5)); background-clip: padding-box; backdrop-filter: blur(24px); } @@ -193,9 +193,7 @@ inset: auto 0 0; padding: 2rem; height: 200px; - background: linear-gradient(to bottom, - transparent 0%, - rgb(var(--background-end-rgb)) 40%); + background: linear-gradient(to bottom, transparent 0%, rgb(var(--background-end-rgb)) 40%); z-index: 1; } } @@ -225,4 +223,4 @@ to { transform: rotate(0deg); } -} \ No newline at end of file +} diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css index 31b5368..c89a275 100644 --- a/frontend/src/styles/globals.css +++ b/frontend/src/styles/globals.css @@ -9,24 +9,19 @@ --background-start-rgb: 214, 219, 220; --background-end-rgb: 255, 255, 255; - --primary-glow: conic-gradient(from 180deg at 50% 50%, - #16abff33 0deg, - #0885ff33 55deg, - #54d6ff33 120deg, - #0071ff33 160deg, - transparent 360deg); - --secondary-glow: radial-gradient(rgba(255, 255, 255, 1), - rgba(255, 255, 255, 0)); + --primary-glow: conic-gradient( + from 180deg at 50% 50%, + #16abff33 0deg, + #0885ff33 55deg, + #54d6ff33 120deg, + #0071ff33 160deg, + transparent 360deg + ); + --secondary-glow: radial-gradient(rgba(255, 255, 255, 1), rgba(255, 255, 255, 0)); --tile-start-rgb: 239, 245, 249; --tile-end-rgb: 228, 232, 233; - --tile-border: conic-gradient(#00000080, - #00000040, - #00000030, - #00000020, - #00000010, - #00000010, - #00000080); + --tile-border: conic-gradient(#00000080, #00000040, #00000030, #00000020, #00000010, #00000010, #00000080); --callout-rgb: 238, 240, 241; --callout-border-rgb: 172, 175, 176; @@ -41,20 +36,11 @@ --background-end-rgb: 0, 0, 0; --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); - --secondary-glow: linear-gradient(to bottom right, - rgba(1, 65, 255, 0), - rgba(1, 65, 255, 0), - rgba(1, 65, 255, 0.3)); + --secondary-glow: linear-gradient(to bottom right, rgba(1, 65, 255, 0), rgba(1, 65, 255, 0), rgba(1, 65, 255, 0.3)); --tile-start-rgb: 2, 13, 46; --tile-end-rgb: 2, 5, 19; - --tile-border: conic-gradient(#ffffff80, - #ffffff40, - #ffffff30, - #ffffff20, - #ffffff10, - #ffffff10, - #ffffff80); + --tile-border: conic-gradient(#ffffff80, #ffffff40, #ffffff30, #ffffff20, #ffffff10, #ffffff10, #ffffff80); --callout-rgb: 20, 20, 20; --callout-border-rgb: 108, 108, 108; @@ -77,10 +63,17 @@ body { body { color: rgb(var(--foreground-rgb)); - background: linear-gradient(to bottom, - transparent, - rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb)); - font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Noto Sans, Ubuntu, Droid Sans, Helvetica Neue, sans-serif; + background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb)); + font-family: + -apple-system, + BlinkMacSystemFont, + Segoe UI, + Roboto, + Noto Sans, + Ubuntu, + Droid Sans, + Helvetica Neue, + sans-serif; } a { @@ -92,4 +85,4 @@ a { html { color-scheme: dark; } -} \ No newline at end of file +} diff --git a/frontend/src/wallets/near.js b/frontend/src/wallets/near.js index 1515ae3..6a066ab 100644 --- a/frontend/src/wallets/near.js +++ b/frontend/src/wallets/near.js @@ -1,18 +1,21 @@ -// near api js -import { providers } from 'near-api-js'; - // wallet selector -import { distinctUntilChanged, map } from 'rxjs'; import '@near-wallet-selector/modal-ui/styles.css'; -import { setupModal } from '@near-wallet-selector/modal-ui'; -import { setupWalletSelector } from '@near-wallet-selector/core'; -import { setupMyNearWallet } from '@near-wallet-selector/my-near-wallet'; + import { setupBitteWallet } from '@near-wallet-selector/bitte-wallet'; +import { setupWalletSelector } from '@near-wallet-selector/core'; +import { setupEthereumWallets } from '@near-wallet-selector/ethereum-wallets'; +import { setupHereWallet } from '@near-wallet-selector/here-wallet'; +import { setupLedger } from '@near-wallet-selector/ledger'; import { setupMeteorWallet } from '@near-wallet-selector/meteor-wallet'; +import { setupModal } from '@near-wallet-selector/modal-ui'; +import { setupMyNearWallet } from '@near-wallet-selector/my-near-wallet'; +import { setupSender } from '@near-wallet-selector/sender'; +// near api js +import { providers, utils } from 'near-api-js'; +import { createContext } from 'react'; // ethereum wallets import { wagmiConfig, web3Modal } from '@/wallets/web3modal'; -import { setupEthereumWallets } from "@near-wallet-selector/ethereum-wallets"; const THIRTY_TGAS = '30000000000000'; const NO_DEPOSIT = '0'; @@ -22,9 +25,9 @@ export class Wallet { * @constructor * @param {Object} options - the options for the wallet * @param {string} options.networkId - the network id to connect to - * @param {string} options.createAccessKeyFor - (optional) create a function call key for a contract + * @param {string} options.createAccessKeyFor - the contract to create an access key for * @example - * const wallet = new Wallet({ networkId: 'testnet'}); + * const wallet = new Wallet({ networkId: 'testnet', createAccessKeyFor: 'contractId' }); * wallet.startUp((signedAccountId) => console.log(signedAccountId)); */ constructor({ networkId = 'testnet', createAccessKeyFor = undefined }) { @@ -35,32 +38,30 @@ export class Wallet { /** * To be called when the website loads * @param {Function} accountChangeHook - a function that is called when the user signs in or out# - * @returns {Promise} - the accountId of the signed-in user + * @returns {Promise} - the accountId of the signed-in user */ startUp = async (accountChangeHook) => { this.selector = setupWalletSelector({ network: this.networkId, modules: [ setupMyNearWallet(), - setupBitteWallet(), + setupHereWallet(), + setupLedger(), setupMeteorWallet(), + setupSender(), + setupBitteWallet(), setupEthereumWallets({ wagmiConfig, web3Modal, alwaysOnboardDuringSignIn: true }), - ] + ], }); const walletSelector = await this.selector; const isSignedIn = walletSelector.isSignedIn(); const accountId = isSignedIn ? walletSelector.store.getState().accounts[0].accountId : ''; - walletSelector.store.observable - .pipe( - map(state => state.accounts), - distinctUntilChanged() - ) - .subscribe(accounts => { - const signedAccount = accounts.find((account) => account.active)?.accountId; - accountChangeHook(signedAccount); - }); + walletSelector.store.observable.subscribe(async (state) => { + const signedAccount = state?.accounts.find((account) => account.active)?.accountId; + accountChangeHook(signedAccount || ''); + }); return accountId; }; @@ -93,7 +94,7 @@ export class Wallet { const url = `https://rpc.${this.networkId}.near.org`; const provider = new providers.JsonRpcProvider({ url }); - let res = await provider.query({ + const res = await provider.query({ request_type: 'call_function', account_id: contractId, method_name: method, @@ -103,7 +104,6 @@ export class Wallet { return JSON.parse(Buffer.from(res.result).toString()); }; - /** * Makes a call to a contract * @param {Object} options - the options for the call @@ -136,7 +136,7 @@ export class Wallet { }; /** - * Retrieves transaction result from the network + * Makes a call to a contract * @param {string} txhash - the transaction hash * @returns {Promise} - the result of the transaction */ @@ -145,7 +145,71 @@ export class Wallet { const { network } = walletSelector.options; const provider = new providers.JsonRpcProvider({ url: network.nodeUrl }); + // Retrieve transaction result from the network const transaction = await provider.txStatus(txhash, 'unnused'); return providers.getTransactionLastResult(transaction); }; + + /** + * Gets the balance of an account + * @param {string} accountId - the account id to get the balance of + * @returns {Promise} - the balance of the account + * + */ + getBalance = async (accountId) => { + const walletSelector = await this.selector; + const { network } = walletSelector.options; + const provider = new providers.JsonRpcProvider({ url: network.nodeUrl }); + + // Retrieve account state from the network + const account = await provider.query({ + request_type: 'view_account', + account_id: accountId, + finality: 'final', + }); + // return amount on NEAR + return account.amount ? Number(utils.format.formatNearAmount(account.amount)) : 0; + }; + + /** + * Signs and sends transactions + * @param {Object[]} transactions - the transactions to sign and send + * @returns {Promise} - the resulting transactions + * + */ + signAndSendTransactions = async ({ transactions }) => { + const selectedWallet = await (await this.selector).wallet(); + return selectedWallet.signAndSendTransactions({ transactions }); + }; + + /** + * + * @param {string} accountId + * @returns {Promise} - the access keys for the + */ + getAccessKeys = async (accountId) => { + const walletSelector = await this.selector; + const { network } = walletSelector.options; + const provider = new providers.JsonRpcProvider({ url: network.nodeUrl }); + + // Retrieve account state from the network + const keys = await provider.query({ + request_type: 'view_access_key_list', + account_id: accountId, + finality: 'final', + }); + return keys.keys; + }; } + +/** + * @typedef NearContext + * @property {import('./wallets/near').Wallet} wallet Current wallet + * @property {string} signedAccountId The AccountId of the signed user + */ + +/** @type {import ('react').Context} */ +export const NearContext = createContext({ + wallet: undefined, + signedAccountId: '', +}); diff --git a/frontend/src/wallets/web3modal.js b/frontend/src/wallets/web3modal.js index 2ea9d91..0ce852e 100644 --- a/frontend/src/wallets/web3modal.js +++ b/frontend/src/wallets/web3modal.js @@ -1,7 +1,8 @@ -import { NetworkId, EVMWalletChain } from '@/config'; -import { reconnect, http, createConfig } from "@wagmi/core"; -import { walletConnect, injected } from "@wagmi/connectors"; -import { createWeb3Modal } from "@web3modal/wagmi"; +import { injected,walletConnect } from '@wagmi/connectors'; +import { createConfig,http, reconnect } from '@wagmi/core'; +import { createWeb3Modal } from '@web3modal/wagmi'; + +import { EVMWalletChain,NetworkId } from '@/config'; // Config const near = { @@ -9,8 +10,8 @@ const near = { name: EVMWalletChain.name, nativeCurrency: { decimals: 18, - name: "NEAR", - symbol: "NEAR", + name: 'NEAR', + symbol: 'NEAR', }, rpcUrls: { default: { http: [EVMWalletChain.rpc] }, @@ -18,11 +19,11 @@ const near = { }, blockExplorers: { default: { - name: "NEAR Explorer", + name: 'NEAR Explorer', url: EVMWalletChain.explorer, }, }, - testnet: NetworkId === "testnet", + testnet: NetworkId === 'testnet', }; // Get your projectId at https://cloud.reown.com @@ -31,14 +32,11 @@ const projectId = '5bb0fe33763b3bea40b8d69e4269b4ae'; export const wagmiConfig = createConfig({ chains: [near], transports: { [near.id]: http() }, - connectors: [ - walletConnect({ projectId, showQrModal: false }), - injected({ shimDisconnect: true }), - ], + connectors: [walletConnect({ projectId, showQrModal: false }), injected({ shimDisconnect: true })], }); // Preserve login state on page reload reconnect(wagmiConfig); // Modal for login -export const web3Modal = createWeb3Modal({ wagmiConfig, projectId }); \ No newline at end of file +export const web3Modal = createWeb3Modal({ wagmiConfig, projectId });