From 5fefee52587625fbe0c1d18fdf1037c5c1200ea7 Mon Sep 17 00:00:00 2001 From: Matias Benary Date: Mon, 23 Dec 2024 13:33:07 -0300 Subject: [PATCH] feat: added vite template --- templates/frontend/vite/.gitignore | 24 ++ templates/frontend/vite/eslint.config.js | 38 +++ templates/frontend/vite/index.html | 13 + templates/frontend/vite/package.json | 59 +++++ templates/frontend/vite/public/vite.svg | 1 + templates/frontend/vite/src/App.jsx | 17 ++ .../frontend/vite/src/assets/near-logo.svg | 43 ++++ templates/frontend/vite/src/assets/near.svg | 1 + templates/frontend/vite/src/assets/react.svg | 1 + .../frontend/vite/src/components/cards.jsx | 26 ++ .../vite/src/components/navigation.jsx | 39 +++ templates/frontend/vite/src/config.js | 24 ++ templates/frontend/vite/src/main.jsx | 32 +++ .../frontend/vite/src/pages/hello_near.jsx | 75 ++++++ templates/frontend/vite/src/pages/home.jsx | 31 +++ .../frontend/vite/src/styles/app.module.css | 226 +++++++++++++++++ .../frontend/vite/src/styles/globals.css | 88 +++++++ templates/frontend/vite/src/wallets/near.js | 228 ++++++++++++++++++ .../frontend/vite/src/wallets/web3modal.js | 42 ++++ templates/frontend/vite/vite.config.js | 27 +++ 20 files changed, 1035 insertions(+) create mode 100644 templates/frontend/vite/.gitignore create mode 100644 templates/frontend/vite/eslint.config.js create mode 100644 templates/frontend/vite/index.html create mode 100644 templates/frontend/vite/package.json create mode 100644 templates/frontend/vite/public/vite.svg create mode 100644 templates/frontend/vite/src/App.jsx create mode 100644 templates/frontend/vite/src/assets/near-logo.svg create mode 100644 templates/frontend/vite/src/assets/near.svg create mode 100644 templates/frontend/vite/src/assets/react.svg create mode 100644 templates/frontend/vite/src/components/cards.jsx create mode 100644 templates/frontend/vite/src/components/navigation.jsx create mode 100644 templates/frontend/vite/src/config.js create mode 100644 templates/frontend/vite/src/main.jsx create mode 100644 templates/frontend/vite/src/pages/hello_near.jsx create mode 100644 templates/frontend/vite/src/pages/home.jsx create mode 100644 templates/frontend/vite/src/styles/app.module.css create mode 100644 templates/frontend/vite/src/styles/globals.css create mode 100644 templates/frontend/vite/src/wallets/near.js create mode 100644 templates/frontend/vite/src/wallets/web3modal.js create mode 100644 templates/frontend/vite/vite.config.js diff --git a/templates/frontend/vite/.gitignore b/templates/frontend/vite/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/templates/frontend/vite/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/templates/frontend/vite/eslint.config.js b/templates/frontend/vite/eslint.config.js new file mode 100644 index 000000000..238d2e4e6 --- /dev/null +++ b/templates/frontend/vite/eslint.config.js @@ -0,0 +1,38 @@ +import js from '@eslint/js' +import globals from 'globals' +import react from 'eslint-plugin-react' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' + +export default [ + { ignores: ['dist'] }, + { + files: ['**/*.{js,jsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + settings: { react: { version: '18.3' } }, + plugins: { + react, + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...js.configs.recommended.rules, + ...react.configs.recommended.rules, + ...react.configs['jsx-runtime'].rules, + ...reactHooks.configs.recommended.rules, + 'react/jsx-no-target-blank': 'off', + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +] diff --git a/templates/frontend/vite/index.html b/templates/frontend/vite/index.html new file mode 100644 index 000000000..0c589eccd --- /dev/null +++ b/templates/frontend/vite/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + + +
+ + + diff --git a/templates/frontend/vite/package.json b/templates/frontend/vite/package.json new file mode 100644 index 000000000..78f08248b --- /dev/null +++ b/templates/frontend/vite/package.json @@ -0,0 +1,59 @@ +{ + "name": "hello-near", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@esbuild-plugins/node-globals-polyfill": "^0.2.3", + "@near-js/providers": "^1.0.1", + "@near-wallet-selector/bitte-wallet": "^8.9.14", + "@near-wallet-selector/core": "^8.9.14", + "@near-wallet-selector/ethereum-wallets": "^8.9.14", + "@near-wallet-selector/here-wallet": "^8.9.14", + "@near-wallet-selector/ledger": "^8.9.14", + "@near-wallet-selector/meteor-wallet": "^8.9.14", + "@near-wallet-selector/modal-ui": "^8.9.14", + "@near-wallet-selector/my-near-wallet": "^8.9.14", + "@near-wallet-selector/near-mobile-wallet": "^8.9.14", + "@near-wallet-selector/sender": "^8.9.14", + "@near-wallet-selector/welldone-wallet": "^8.9.14", + "@wagmi/connectors": "^5.7.1", + "@wagmi/core": "^2.16.1", + "@web3modal/wagmi": "^5.1.11", + "bootstrap": "^5", + "bootstrap-icons": "^1.11.3", + "buffer": "^6.0.3", + "near-api-js": "^5.0.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^7.1.0", + "wagmi": "^2.14.4", + "wouter": "^3.3.5" + }, + "overrides": { + "@near-wallet-selector/ethereum-wallets": { + "near-api-js": "4.0.3" + } + }, + "devDependencies": { + "@eslint/js": "^9.17.0", + "@types/node": "^22.10.1", + "@types/react": "^18.3.17", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react-swc": "^3.5.0", + "encoding": "^0.1.13", + "eslint": "^9.17.0", + "eslint-plugin-react": "^7.37.2", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.16", + "globals": "^15.13.0", + "vite": "^6.0.3", + "vite-plugin-eslint": "^1.8.1" + } +} \ No newline at end of file diff --git a/templates/frontend/vite/public/vite.svg b/templates/frontend/vite/public/vite.svg new file mode 100644 index 000000000..e7b8dfb1b --- /dev/null +++ b/templates/frontend/vite/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/frontend/vite/src/App.jsx b/templates/frontend/vite/src/App.jsx new file mode 100644 index 000000000..514afd10a --- /dev/null +++ b/templates/frontend/vite/src/App.jsx @@ -0,0 +1,17 @@ +import { Navigation } from './components/navigation'; +import Home from './pages/home'; +import { Route } from 'wouter'; +import HelloNear from './pages/hello_near'; + +function App() { + + return ( + <> + + + + + ) +} + +export default App diff --git a/templates/frontend/vite/src/assets/near-logo.svg b/templates/frontend/vite/src/assets/near-logo.svg new file mode 100644 index 000000000..50443c0ef --- /dev/null +++ b/templates/frontend/vite/src/assets/near-logo.svg @@ -0,0 +1,43 @@ + + + + + + + diff --git a/templates/frontend/vite/src/assets/near.svg b/templates/frontend/vite/src/assets/near.svg new file mode 100644 index 000000000..acec6fc15 --- /dev/null +++ b/templates/frontend/vite/src/assets/near.svg @@ -0,0 +1 @@ + diff --git a/templates/frontend/vite/src/assets/react.svg b/templates/frontend/vite/src/assets/react.svg new file mode 100644 index 000000000..6c87de9bb --- /dev/null +++ b/templates/frontend/vite/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/frontend/vite/src/components/cards.jsx b/templates/frontend/vite/src/components/cards.jsx new file mode 100644 index 000000000..f37ac9d95 --- /dev/null +++ b/templates/frontend/vite/src/components/cards.jsx @@ -0,0 +1,26 @@ +import styles from '@/styles/app.module.css'; + +export const Cards = () => { + return ( +
+ +

+ Near Docs -> +

+

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

+
+ + +

+ Near Integration -> +

+

Discover how simple it is to interact with a Near smart contract.

+
+
+ ); +}; diff --git a/templates/frontend/vite/src/components/navigation.jsx b/templates/frontend/vite/src/components/navigation.jsx new file mode 100644 index 000000000..a995d9e2e --- /dev/null +++ b/templates/frontend/vite/src/components/navigation.jsx @@ -0,0 +1,39 @@ +import { useContext,useEffect, useState } from 'react'; + +import NearLogo from '@/assets/near-logo.svg'; +import { NearContext } from '@/wallets/near'; +import { Link } from 'wouter'; +import styles from '@/styles/app.module.css'; + +export const Navigation = () => { + const { signedAccountId, wallet } = useContext(NearContext); + const [action, setAction] = useState(() => {}); + const [label, setLabel] = useState('Loading...'); + + useEffect(() => { + if (!wallet) return; + + if (signedAccountId) { + setAction(() => wallet.signOut); + setLabel(`Logout ${signedAccountId}`); + } else { + setAction(() => wallet.signIn); + setLabel('Login'); + } + }, [signedAccountId, wallet]); + + return ( + + ); +}; diff --git a/templates/frontend/vite/src/config.js b/templates/frontend/vite/src/config.js new file mode 100644 index 000000000..bbdaaf575 --- /dev/null +++ b/templates/frontend/vite/src/config.js @@ -0,0 +1,24 @@ +const contractPerNetwork = { + mainnet: 'hello.near-examples.near', + testnet: 'hello.near-examples.testnet', +}; + +// 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 NetworkId = 'testnet'; +export const HelloNearContract = contractPerNetwork[NetworkId]; +export const EVMWalletChain = evmWalletChains[NetworkId]; diff --git a/templates/frontend/vite/src/main.jsx b/templates/frontend/vite/src/main.jsx new file mode 100644 index 000000000..f91decbd3 --- /dev/null +++ b/templates/frontend/vite/src/main.jsx @@ -0,0 +1,32 @@ +import { StrictMode, useEffect, useState } from 'react'; +import { createRoot } from 'react-dom/client'; +import './styles/globals.css'; +import { NearContext, Wallet } from '@/wallets/near'; +import App from './App.jsx'; +import { NetworkId } from './config.js'; + +// Wallet instance +const wallet = new Wallet({ NetworkId: NetworkId }); + +// Optional: Create an access key so the user does not need to sign transactions. Read more about access keys here: https://docs.near.org/concepts/protocol/access-keys +// const wallet = new Wallet({ networkId: NetworkId, createAccessKeyFor: HelloNearContract }); + +const Root = () => { + const [signedAccountId, setSignedAccountId] = useState(null); + + useEffect(() => { + wallet.startUp(setSignedAccountId); + }, []); + + return ( + + + + ); +}; + +createRoot(document.getElementById('root')).render( + + + , +); \ No newline at end of file diff --git a/templates/frontend/vite/src/pages/hello_near.jsx b/templates/frontend/vite/src/pages/hello_near.jsx new file mode 100644 index 000000000..38911c8a7 --- /dev/null +++ b/templates/frontend/vite/src/pages/hello_near.jsx @@ -0,0 +1,75 @@ +import { useContext, useEffect, useState } from 'react'; + +import { Cards } from '@/components/cards'; +import styles from '@/styles/app.module.css'; +import { NearContext } from '@/wallets/near'; + +import { HelloNearContract } from '@/config'; + +// Contract that the app will interact with +const CONTRACT = HelloNearContract; + +export default function HelloNear() { + const { signedAccountId, wallet } = useContext(NearContext); + + const [greeting, setGreeting] = useState('loading...'); + const [newGreeting, setNewGreeting] = useState('loading...'); + const [loggedIn, setLoggedIn] = useState(false); + const [showSpinner, setShowSpinner] = useState(false); + + useEffect(() => { + if (!wallet) return; + + wallet.viewMethod({ contractId: CONTRACT, method: 'get_greeting' }).then((greeting) => setGreeting(greeting)); + }, [wallet]); + + useEffect(() => { + setLoggedIn(!!signedAccountId); + }, [signedAccountId]); + + const saveGreeting = async () => { + wallet.callMethod({ contractId: CONTRACT, method: 'set_greeting', args: { greeting: newGreeting } }) + .catch(async () => { + wallet.viewMethod({ contractId: CONTRACT, method: 'get_greeting' }).then((greeting) => setGreeting(greeting)); + }); + + setShowSpinner(true); + await new Promise(resolve => setTimeout(resolve, 300)); + setGreeting(newGreeting); + setShowSpinner(false); + }; + + return ( +
+
+

+ Interacting with the contract:   + {CONTRACT} +

+
+
+

+ The contract says: {greeting} +

+ + +
+ +
+ ); +} diff --git a/templates/frontend/vite/src/pages/home.jsx b/templates/frontend/vite/src/pages/home.jsx new file mode 100644 index 000000000..2b8bd1f10 --- /dev/null +++ b/templates/frontend/vite/src/pages/home.jsx @@ -0,0 +1,31 @@ +import styles from '@/styles/app.module.css'; +import NearLogo from '@/assets/near-logo.svg'; +import NextLogo from '@/assets/react.svg'; +import { Cards } from '@/components/cards'; + +const Home = () => { + return ( + +
+
+ +
+ NEAR Logo +

+

+ React Logo +
+ +
+ +
+
+ ) +} + +export default Home diff --git a/templates/frontend/vite/src/styles/app.module.css b/templates/frontend/vite/src/styles/app.module.css new file mode 100644 index 000000000..6ad622306 --- /dev/null +++ b/templates/frontend/vite/src/styles/app.module.css @@ -0,0 +1,226 @@ +.main { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + padding: 6rem; + min-height: 100vh; +} + +.description { + display: inherit; + justify-content: inherit; + align-items: inherit; + font-size: 0.85rem; + max-width: var(--max-width); + width: 100%; + z-index: 2; + font-family: var(--font-mono); +} + +.description a { + display: flex; + justify-content: center; + align-items: center; + gap: 0.5rem; +} + +.description p { + position: relative; + margin: 0; + padding: 1rem; + background-color: rgba(var(--callout-rgb), 0.5); + border: 1px solid rgba(var(--callout-border-rgb), 0.3); + border-radius: var(--border-radius); +} + +.code { + font-weight: 700; + font-family: var(--font-mono); +} + +.grid { + display: grid; + grid-template-columns: repeat(2, minmax(25%, auto)); + max-width: 100%; + width: var(--max-width); +} + +.card { + padding: 1rem 1.2rem; + 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; +} + +.card span { + display: inline-block; + transition: transform 200ms; +} + +.card h2 { + font-weight: 600; + margin-bottom: 0.7rem; +} + +.card p { + margin: 0; + opacity: 0.6; + font-size: 0.9rem; + line-height: 1.5; + max-width: 30ch; +} + +.center { + display: flex; + justify-content: center; + align-items: center; + position: relative; + padding: 4rem 0; + width: 100%; +} + +.center::before { + background: var(--secondary-glow); + border-radius: 50%; + width: 480px; + height: 360px; + margin-left: -400px; +} + +.center::after { + background: var(--primary-glow); + width: 240px; + height: 180px; + z-index: -1; +} + +.center::before, +.center::after { + content: ''; + left: 50%; + position: absolute; + filter: blur(45px); + transform: translateZ(0); + opacity: 0.3; +} + +.logo { + position: relative; +} + +/* Enable hover only on non-touch devices */ +@media (hover: hover) and (pointer: fine) { + .card:hover { + background: rgba(var(--card-rgb), 0.1); + border: 1px solid rgba(var(--card-border-rgb), 0.15); + } + + .card:hover span { + transform: translateX(4px); + } +} + +@media (prefers-reduced-motion) { + .card:hover span { + transform: none; + } +} + +/* Mobile */ +@media (max-width: 700px) { + .content { + padding: 4rem; + } + + .grid { + grid-template-columns: 1fr; + margin-bottom: 120px; + max-width: 320px; + text-align: center; + } + + .card { + padding: 1rem 2.5rem; + } + + .card h2 { + margin-bottom: 0.5rem; + } + + .center { + padding: 8rem 0 6rem; + } + + .center::before { + transform: none; + height: 300px; + } + + .description { + font-size: 0.8rem; + } + + .description a { + padding: 1rem; + } + + .description p, + .description div { + display: flex; + justify-content: center; + width: 100%; + } + + .description p { + align-items: center; + inset: 0 0 auto; + padding: 2rem 1rem 1.4rem; + 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-clip: padding-box; + backdrop-filter: blur(24px); + } + + .description div { + align-items: flex-end; + pointer-events: none; + inset: auto 0 0; + padding: 2rem; + height: 200px; + background: linear-gradient(to bottom, transparent 0%, rgb(var(--background-end-rgb)) 40%); + z-index: 1; + } +} + +/* Tablet and Smaller Desktop */ +@media (min-width: 701px) and (max-width: 1120px) { + .grid { + grid-template-columns: repeat(2, 50%); + } +} + +@media (prefers-color-scheme: dark) { + .reactLogo { + filter: drop-shadow(0 0 0.3rem #ffffff70); + } + + .logo { + filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); + } +} + +@keyframes rotate { + from { + transform: rotate(360deg); + } + + to { + transform: rotate(0deg); + } +} diff --git a/templates/frontend/vite/src/styles/globals.css b/templates/frontend/vite/src/styles/globals.css new file mode 100644 index 000000000..c89a2752b --- /dev/null +++ b/templates/frontend/vite/src/styles/globals.css @@ -0,0 +1,88 @@ +@import 'bootstrap'; +@import 'bootstrap-icons'; + +:root { + --max-width: 1100px; + --border-radius: 12px; + + --foreground-rgb: 0, 0, 0; + --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)); + + --tile-start-rgb: 239, 245, 249; + --tile-end-rgb: 228, 232, 233; + --tile-border: conic-gradient(#00000080, #00000040, #00000030, #00000020, #00000010, #00000010, #00000080); + + --callout-rgb: 238, 240, 241; + --callout-border-rgb: 172, 175, 176; + --card-rgb: 180, 185, 188; + --card-border-rgb: 131, 134, 135; +} + +@media (prefers-color-scheme: dark) { + :root { + --foreground-rgb: 255, 255, 255; + --background-start-rgb: 0, 0, 0; + --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)); + + --tile-start-rgb: 2, 13, 46; + --tile-end-rgb: 2, 5, 19; + --tile-border: conic-gradient(#ffffff80, #ffffff40, #ffffff30, #ffffff20, #ffffff10, #ffffff10, #ffffff80); + + --callout-rgb: 20, 20, 20; + --callout-border-rgb: 108, 108, 108; + --card-rgb: 100, 100, 100; + --card-border-rgb: 200, 200, 200; + } +} + +* { + box-sizing: border-box; + padding: 0; + margin: 0; +} + +html, +body { + max-width: 100vw; + overflow-x: hidden; +} + +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; +} + +a { + color: inherit; + text-decoration: none; +} + +@media (prefers-color-scheme: dark) { + html { + color-scheme: dark; + } +} diff --git a/templates/frontend/vite/src/wallets/near.js b/templates/frontend/vite/src/wallets/near.js new file mode 100644 index 000000000..3e084e92c --- /dev/null +++ b/templates/frontend/vite/src/wallets/near.js @@ -0,0 +1,228 @@ +// wallet selector +import '@near-wallet-selector/modal-ui/styles.css'; + +import { setupBitteWallet } from '@near-wallet-selector/bitte-wallet'; +import { setupWalletSelector } from '@near-wallet-selector/core'; +import { setupEthereumWallets } from '@near-wallet-selector/ethereum-wallets'; +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'; +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 { Buffer } from 'buffer'; + + +// near api js +import { providers, utils } from 'near-api-js'; +import { createContext } from 'react'; + +// ethereum wallets +import { wagmiConfig, web3Modal } from '@/wallets/web3modal'; + +const THIRTY_TGAS = '30000000000000'; +const NO_DEPOSIT = '0'; + +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 - the contract to create an access key for + * @example + * const wallet = new Wallet({ networkId: 'testnet', createAccessKeyFor: 'contractId' }); + * wallet.startUp((signedAccountId) => console.log(signedAccountId)); + */ + constructor({ networkId = 'testnet', createAccessKeyFor = undefined }) { + this.createAccessKeyFor = createAccessKeyFor; + this.networkId = networkId; + } + + /** + * 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 + */ + startUp = async (accountChangeHook) => { + this.selector = setupWalletSelector({ + network: this.networkId, + modules: [ + setupMeteorWallet(), + setupEthereumWallets({ wagmiConfig, web3Modal, alwaysOnboardDuringSignIn: true }), + setupLedger(), + setupBitteWallet(), + setupHereWallet(), + setupSender(), + setupNearMobileWallet(), + setupWelldoneWallet(), + setupMyNearWallet(), + ], + }); + + const walletSelector = await this.selector; + const isSignedIn = walletSelector.isSignedIn(); + const accountId = isSignedIn ? walletSelector.store.getState().accounts[0].accountId : ''; + + walletSelector.store.observable.subscribe(async (state) => { + const signedAccount = state?.accounts.find((account) => account.active)?.accountId; + accountChangeHook(signedAccount || ''); + }); + + return accountId; + }; + + /** + * Displays a modal to login the user + */ + signIn = async () => { + const modal = setupModal(await this.selector, { contractId: this.createAccessKeyFor }); + modal.show(); + }; + + /** + * Logout the user + */ + signOut = async () => { + const selectedWallet = await (await this.selector).wallet(); + selectedWallet.signOut(); + }; + + /** + * Makes a read-only call to a contract + * @param {Object} options - the options for the call + * @param {string} options.contractId - the contract's account id + * @param {string} options.method - the method to call + * @param {Object} options.args - the arguments to pass to the method + * @returns {Promise} - the result of the method call + */ + viewMethod = async ({ contractId, method, args = {} }) => { + const url = `https://rpc.${this.networkId}.near.org`; + const provider = new providers.JsonRpcProvider({ url }); + + const res = await provider.query({ + request_type: 'call_function', + account_id: contractId, + method_name: method, + args_base64: Buffer.from(JSON.stringify(args)).toString('base64'), + finality: 'optimistic', + }); + return JSON.parse(Buffer.from(res.result).toString()); + }; + + /** + * Makes a call to a contract + * @param {Object} options - the options for the call + * @param {string} options.contractId - the contract's account id + * @param {string} options.method - the method to call + * @param {Object} options.args - the arguments to pass to the method + * @param {string} options.gas - the amount of gas to use + * @param {string} options.deposit - the amount of yoctoNEAR to deposit + * @returns {Promise} - the resulting transaction + */ + callMethod = async ({ contractId, method, args = {}, gas = THIRTY_TGAS, deposit = NO_DEPOSIT }) => { + // Sign a transaction with the "FunctionCall" action + const selectedWallet = await (await this.selector).wallet(); + const outcome = await selectedWallet.signAndSendTransaction({ + receiverId: contractId, + actions: [ + { + type: 'FunctionCall', + params: { + methodName: method, + args, + gas, + deposit, + }, + }, + ], + }); + + return providers.getTransactionLastResult(outcome); + }; + + /** + * Makes a call to a contract + * @param {string} txhash - the transaction hash + * @returns {Promise} - the result of the transaction + */ + getTransactionResult = async (txhash) => { + const walletSelector = await this.selector; + 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 + * @param {boolean} format - whether to format the balance + * @returns {Promise} - the balance of the account + * + */ + getBalance = async (accountId, format = false) => { + 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', + }); + + // Format the amount if needed + if (format) { + return account.amount ? utils.format.formatNearAmount(account.amount) : '0'; + } else { + return 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/templates/frontend/vite/src/wallets/web3modal.js b/templates/frontend/vite/src/wallets/web3modal.js new file mode 100644 index 000000000..0ce852e4d --- /dev/null +++ b/templates/frontend/vite/src/wallets/web3modal.js @@ -0,0 +1,42 @@ +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 = { + id: EVMWalletChain.chainId, + name: EVMWalletChain.name, + nativeCurrency: { + decimals: 18, + name: 'NEAR', + symbol: 'NEAR', + }, + rpcUrls: { + default: { http: [EVMWalletChain.rpc] }, + public: { http: [EVMWalletChain.rpc] }, + }, + blockExplorers: { + default: { + name: 'NEAR Explorer', + url: EVMWalletChain.explorer, + }, + }, + testnet: NetworkId === 'testnet', +}; + +// Get your projectId at https://cloud.reown.com +const projectId = '5bb0fe33763b3bea40b8d69e4269b4ae'; + +export const wagmiConfig = createConfig({ + chains: [near], + transports: { [near.id]: http() }, + 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 }); diff --git a/templates/frontend/vite/vite.config.js b/templates/frontend/vite/vite.config.js new file mode 100644 index 000000000..32ad034da --- /dev/null +++ b/templates/frontend/vite/vite.config.js @@ -0,0 +1,27 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react-swc' +import eslint from 'vite-plugin-eslint'; +import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill'; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react(),eslint()], + resolve: { + alias: { + '@': '/src', + buffer: 'buffer', + }, + }, + optimizeDeps: { + esbuildOptions: { + define: { + global: 'globalThis' + }, + plugins: [ + NodeGlobalsPolyfillPlugin({ + buffer: true + }) + ] + } +} +}) \ No newline at end of file