From db34559516f8eb4a83d9b6303356f793a3cb7f87 Mon Sep 17 00:00:00 2001 From: Matias Benary Date: Mon, 6 Jan 2025 12:21:02 -0300 Subject: [PATCH 1/3] feat(ui): update packages and improve UX/UI --- frontend/package.json | 32 +++- frontend/src/components/DonationBox.jsx | 42 ++--- frontend/src/components/DonationForm.jsx | 132 ++++++++-------- frontend/src/components/DonationsTable.jsx | 141 ++++++++--------- frontend/src/components/MyDonation.jsx | 51 ++++++ frontend/src/components/Navigation.jsx | 34 ++-- frontend/src/config.js | 20 ++- frontend/src/context.js | 6 +- frontend/src/pages/_app.js | 27 ++-- frontend/src/pages/index.js | 31 ++-- frontend/src/styles/globals.css | 84 ++++++---- frontend/src/wallets/near.js | 171 +++++++++++++++++---- frontend/src/wallets/web3modal.js | 45 ++++++ 13 files changed, 546 insertions(+), 270 deletions(-) create mode 100644 frontend/src/components/MyDonation.jsx create mode 100644 frontend/src/wallets/web3modal.js diff --git a/frontend/package.json b/frontend/package.json index ae16101..f5c3482 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,22 +12,38 @@ "lint": "next lint" }, "dependencies": { - "@near-wallet-selector/core": "^8.9.11", - "@near-wallet-selector/here-wallet": "^8.9.11", - "@near-wallet-selector/modal-ui": "^8.9.11", - "@near-wallet-selector/my-near-wallet": "^8.9.11", + "@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", + "@web3modal/wagmi": "^5.1.11", "bootstrap": "^5", "bootstrap-icons": "^1.11.3", - "near-api-js": "^4.0.3", - "next": "14.2.3", + "near-api-js": "^5.0.1", + "next": "14.2.5", "react": "^18", - "react-dom": "^18" + "react-dom": "^18", + "wagmi": "^2.13.3" + }, + "overrides": { + "@near-wallet-selector/ethereum-wallets": { + "near-api-js": "4.0.3" + } }, "resolutions": { "near-api-js": "4.0.3" }, "devDependencies": { - "eslint": "^8", + "encoding": "^0.1.13", + "eslint": "^9", "eslint-config-next": "14.2.3" } } diff --git a/frontend/src/components/DonationBox.jsx b/frontend/src/components/DonationBox.jsx index bf7d8dd..ccdb5fe 100644 --- a/frontend/src/components/DonationBox.jsx +++ b/frontend/src/components/DonationBox.jsx @@ -1,28 +1,28 @@ import { useContext } from "react"; import DonationForm from "./DonationForm"; -import { NearContext } from '@/context'; +import { NearContext } from "@/context"; -const DonationBox = () => { - const { signedAccountId, wallet } = useContext(NearContext); +const DonationBox = ({ setMyDonation }) => { + const { signedAccountId } = useContext(NearContext); - return ( -
-
-

- Donate to -

-
-
- {signedAccountId ? ( - - ) : ( -

- Please sign in with your NEAR wallet to make a donation. -

- )} -
-
- ); + return ( +
+
+

+ Donate to +

+
+
+ {signedAccountId ? ( + + ) : ( +

+ Please sign in with your NEAR wallet to make a donation. +

+ )} +
+
+ ); }; export default DonationBox; diff --git a/frontend/src/components/DonationForm.jsx b/frontend/src/components/DonationForm.jsx index 2260072..0f3aab9 100644 --- a/frontend/src/components/DonationForm.jsx +++ b/frontend/src/components/DonationForm.jsx @@ -4,78 +4,74 @@ import { useState, useContext } from "react"; import { NearContext } from "@/context"; import { DonationNearContract } from "@/config"; -const DonationForm = () => { - const { wallet } = useContext(NearContext); - const [amount, setAmount] = useState(0); +const DonationForm = ({ setMyDonation }) => { + const { wallet } = useContext(NearContext); + 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", - ).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 setDonation = async (amount) => { + let 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 handleSubmit = async (event) => { - event.preventDefault(); + const handleSubmit = async (event) => { + event.preventDefault(); - try { - let deposit = utils.format.parseNearAmount(amount.toString()); - await wallet.callMethod({ - contractId: DonationNearContract, - method: "donate", - deposit, - }); - } catch (e) { - alert( - "Something went wrong! " + - "Maybe you need to sign out and back in? " + - "Check your browser console for more info.", - ); - throw e; - } - }; + let deposit = utils.format.parseNearAmount(amount.toString()); + setMyDonation(amount); + wallet + .callMethod({ + contractId: DonationNearContract, + method: "donate", + deposit, + }) + .catch(() => { + setMyDonation(-Number(amount)); + }); + }; - return ( - <> -
- {[10, 20, 50, 100].map((amount) => ( -
- -
- ))} -
-
-
- -
- setAmount(e.target.value)} - className="form-control" - /> - -
-
- -
- - ); + return ( + <> +
+ {[10, 20, 50, 100].map((amount) => ( +
+ +
+ ))} +
+
+
+ +
+ setAmount(e.target.value)} + className="form-control" + /> + +
+
+ +
+ + ); }; export default DonationForm; diff --git a/frontend/src/components/DonationsTable.jsx b/frontend/src/components/DonationsTable.jsx index b64c5ea..36d9c1e 100644 --- a/frontend/src/components/DonationsTable.jsx +++ b/frontend/src/components/DonationsTable.jsx @@ -5,82 +5,83 @@ import { DonationNearContract } from "@/config"; import { NearContext } from "@/context"; const DonationsTable = () => { - const { wallet } = useContext(NearContext); - const [donations, setDonations] = useState([]); - const [currentPage, setCurrentPage] = useState(1); - const [lastPage, setLastPage] = useState(0); - const donationsPerPage = 5; + const { wallet } = useContext(NearContext); + 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 wallet.viewMethod({ - contractId: DonationNearContract, - method: "number_of_donors", - }); + const getDonations = async (page) => { + const number_of_donors = await wallet.viewMethod({ + contractId: DonationNearContract, + method: "number_of_donors", + }); - setLastPage(Math.ceil(number_of_donors / donationsPerPage)); - const fromIndex = (page - 1) * donationsPerPage; - const donations = await wallet.viewMethod({ - contractId: DonationNearContract, - method: "get_donations", - args: { - from_index: fromIndex.toString(), - limit: donationsPerPage.toString(), - }, - }); - return donations; - }; + setLastPage(Math.ceil(number_of_donors / donationsPerPage)); + const fromIndex = (page - 1) * donationsPerPage; + const donations = await wallet.viewMethod({ + contractId: DonationNearContract, + method: "get_donations", + args: { + from_index: fromIndex.toString(), + limit: donationsPerPage.toString(), + }, + }); + return donations; + }; - useEffect(() => { - if (!wallet) return; - getDonations(currentPage) - .then(loadedDonations => setDonations(loadedDonations)); - }, [wallet, currentPage]); + useEffect(() => { + if (!wallet) return; + getDonations(currentPage).then((loadedDonations) => + setDonations(loadedDonations), + ); + }, [wallet, currentPage]); - const goToNextPage = () => { - setCurrentPage(prevPage => prevPage + 1); - }; + const goToNextPage = () => { + setCurrentPage((prevPage) => prevPage + 1); + }; - const goToPrevPage = () => { - setCurrentPage(prevPage => prevPage - 1); - }; + const goToPrevPage = () => { + setCurrentPage((prevPage) => prevPage - 1); + }; - return ( -
- - - - - - - - - {donations.map((donation) => ( - - - - - ))} - -
UserTotal Donated Ⓝ
{donation.account_id}{utils.format.formatNearAmount(donation.total_amount)}
-
- - Page {currentPage} - -
-
- ); + return ( +
+ + + + + + + + + {donations.map((donation) => ( + + + + + ))} + +
UserTotal Donated Ⓝ
{donation.account_id}{utils.format.formatNearAmount(donation.total_amount)}
+
+ + Page {currentPage} + +
+
+ ); }; export default DonationsTable; diff --git a/frontend/src/components/MyDonation.jsx b/frontend/src/components/MyDonation.jsx new file mode 100644 index 0000000..c80b728 --- /dev/null +++ b/frontend/src/components/MyDonation.jsx @@ -0,0 +1,51 @@ +import { useContext, useEffect, useState } from "react"; +import { NearContext } from "@/context"; +import { DonationNearContract } from "@/config"; +import { utils } from "near-api-js"; + +const MyDonation = ({ myDonation }) => { + const { signedAccountId, wallet } = useContext(NearContext); + const [donation, setDonation] = useState(0); + + useEffect(() => { + if (!myDonation) return; + + setDonation( + Math.round((Number(donation) + Number(myDonation)) * 100) / 100, + ); + }, [myDonation]); + + useEffect(() => { + if (!wallet && !signedAccountId) return; + const getMyDonations = async () => { + if (signedAccountId.trim() === "") return; + console.log("Getting donations for account: ", signedAccountId); + const loadedDonation = await wallet.viewMethod({ + contractId: DonationNearContract, + method: "get_donation_for_account", + args: { + account_id: signedAccountId, + }, + }); + + setDonation(utils.format.formatNearAmount(loadedDonation.total_amount)); + }; + getMyDonations(); + }, [wallet, signedAccountId]); + + return ( + <> + {signedAccountId ? ( +

+ You have donated {donation} NEAR to the cause. +

+ ) : ( +

+ Please sign in with your NEAR wallet to make a donation. +

+ )} + + ); +}; + +export default MyDonation; diff --git a/frontend/src/components/Navigation.jsx b/frontend/src/components/Navigation.jsx index 6849b8b..6246013 100644 --- a/frontend/src/components/Navigation.jsx +++ b/frontend/src/components/Navigation.jsx @@ -1,14 +1,14 @@ -import Image from 'next/image'; -import Link from 'next/link'; -import { useEffect, useState, useContext } from 'react'; +import Image from "next/image"; +import Link from "next/link"; +import { useEffect, useState, useContext } from "react"; -import { NearContext } from '@/context'; -import NearLogo from '/public/near-logo.svg'; +import { NearContext } from "@/context"; +import NearLogo from "/public/near-logo.svg"; export const Navigation = () => { const { signedAccountId, wallet } = useContext(NearContext); - const [action, setAction] = useState(() => { }); - const [label, setLabel] = useState('Loading...'); + const [action, setAction] = useState(() => {}); + const [label, setLabel] = useState("Loading..."); useEffect(() => { if (!wallet) return; @@ -18,7 +18,7 @@ export const Navigation = () => { setLabel(`Logout ${signedAccountId}`); } else { setAction(() => wallet.signIn); - setLabel('Login'); + setLabel("Login"); } }, [signedAccountId, wallet]); @@ -26,12 +26,22 @@ export const Navigation = () => { ); -}; \ No newline at end of file +}; diff --git a/frontend/src/config.js b/frontend/src/config.js index 21b531b..7fa2cb6 100644 --- a/frontend/src/config.js +++ b/frontend/src/config.js @@ -1,6 +1,24 @@ const contractPerNetwork = { - testnet: "donation.near-examples.testnet", + 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/context.js b/frontend/src/context.js index 7422309..6678cfd 100644 --- a/frontend/src/context.js +++ b/frontend/src/context.js @@ -1,4 +1,4 @@ -import { createContext } from 'react'; +import { createContext } from "react"; /** * @typedef NearContext @@ -9,5 +9,5 @@ import { createContext } from 'react'; /** @type {import ('react').Context} */ export const NearContext = createContext({ wallet: undefined, - signedAccountId: '' -}); \ No newline at end of file + signedAccountId: "", +}); diff --git a/frontend/src/pages/_app.js b/frontend/src/pages/_app.js index d223766..78bd9eb 100644 --- a/frontend/src/pages/_app.js +++ b/frontend/src/pages/_app.js @@ -1,18 +1,27 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState } from "react"; -import '@/styles/globals.css'; -import { NearContext } from '@/context'; -import { Navigation } from '@/components/Navigation'; +import "@/styles/globals.css"; +import { NearContext } from "@/context"; +import { Navigation } from "@/components/Navigation"; -import { Wallet } from '@/wallets/near'; -import { NetworkId } from '@/config'; +import { Wallet } from "@/wallets/near"; +import { DonationNearContract, NetworkId } from "@/config"; +// 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({ +// createAccessKeyFor: DonationNearContract, +// networkId: NetworkId, +// }); + export default function MyApp({ Component, pageProps }) { - const [signedAccountId, setSignedAccountId] = useState(''); + const [signedAccountId, setSignedAccountId] = useState(""); - useEffect(() => { wallet.startUp(setSignedAccountId) }, []); + useEffect(() => { + wallet.startUp(setSignedAccountId); + }, []); return ( @@ -20,4 +29,4 @@ export default function MyApp({ Component, pageProps }) { ); -} \ No newline at end of file +} diff --git a/frontend/src/pages/index.js b/frontend/src/pages/index.js index 4dd6861..7706c32 100644 --- a/frontend/src/pages/index.js +++ b/frontend/src/pages/index.js @@ -1,18 +1,23 @@ import DonationBox from "@/components/DonationBox"; import DonationsTable from "@/components/DonationsTable"; +import MyDonation from "@/components/MyDonation"; +import { useState } from "react"; export default function Home() { - return ( -
-
-
-

Latest Donations

- -
-
- -
-
-
- ); + const [myDonation, setMyDonation] = useState(0); + return ( +
+
+
+

My Donation

+ +

Latest Donations

+ +
+
+ +
+
+
+ ); } diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css index 674e856..b374c94 100644 --- a/frontend/src/styles/globals.css +++ b/frontend/src/styles/globals.css @@ -1,5 +1,5 @@ -@import 'bootstrap'; -@import 'bootstrap-icons'; +@import "bootstrap"; +@import "bootstrap-icons"; :root { --max-width: 1100px; @@ -9,24 +9,30 @@ --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 +47,24 @@ --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; @@ -78,10 +88,22 @@ body { body { color: rgb(var(--foreground-rgb)); - background: linear-gradient(to bottom, + 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; + 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 { diff --git a/frontend/src/wallets/near.js b/frontend/src/wallets/near.js index f620195..d01da0d 100644 --- a/frontend/src/wallets/near.js +++ b/frontend/src/wallets/near.js @@ -1,16 +1,27 @@ +// 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"; + // near api js -import { providers } from 'near-api-js'; +import { providers, utils } from "near-api-js"; +import { createContext } from "react"; -// 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 { setupHereWallet } from '@near-wallet-selector/here-wallet'; -import { setupMyNearWallet } from '@near-wallet-selector/my-near-wallet'; +// ethereum wallets +import { wagmiConfig, web3Modal } from "@/wallets/web3modal"; -const THIRTY_TGAS = '30000000000000'; -const NO_DEPOSIT = '0'; +const THIRTY_TGAS = "30000000000000"; +const NO_DEPOSIT = "0"; export class Wallet { /** @@ -22,7 +33,7 @@ export class Wallet { * const wallet = new Wallet({ networkId: 'testnet', createAccessKeyFor: 'contractId' }); * wallet.startUp((signedAccountId) => console.log(signedAccountId)); */ - constructor({ networkId = 'testnet', createAccessKeyFor = undefined }) { + constructor({ networkId = "testnet", createAccessKeyFor = undefined }) { this.createAccessKeyFor = createAccessKeyFor; this.networkId = networkId; } @@ -30,27 +41,40 @@ 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(), setupHereWallet()] + 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 - .pipe( - map(state => state.accounts), - distinctUntilChanged() - ) - .subscribe(accounts => { - const signedAccount = accounts.find((account) => account.active)?.accountId; - accountChangeHook(signedAccount); - }); + 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; }; @@ -59,7 +83,9 @@ export class Wallet { * Displays a modal to login the user */ signIn = async () => { - const modal = setupModal(await this.selector, { contractId: this.createAccessKeyFor }); + const modal = setupModal(await this.selector, { + contractId: this.createAccessKeyFor, + }); modal.show(); }; @@ -83,17 +109,16 @@ export class Wallet { const url = `https://rpc.${this.networkId}.near.org`; const provider = new providers.JsonRpcProvider({ url }); - let res = await provider.query({ - request_type: 'call_function', + 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', + 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 @@ -104,14 +129,20 @@ export class Wallet { * @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 }) => { + 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', + type: "FunctionCall", params: { methodName: method, args, @@ -126,7 +157,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 */ @@ -135,7 +166,79 @@ export class Wallet { const { network } = walletSelector.options; const provider = new providers.JsonRpcProvider({ url: network.nodeUrl }); - const transaction = await provider.txStatus(txhash, 'unnused'); + // Retrieve transaction result from the network + const transaction = await provider.txStatus(txhash, "unused"); 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/frontend/src/wallets/web3modal.js b/frontend/src/wallets/web3modal.js new file mode 100644 index 0000000..a01ad8d --- /dev/null +++ b/frontend/src/wallets/web3modal.js @@ -0,0 +1,45 @@ +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 }); From 7b62e516d167a3e40703f0ad762149f4e173431e Mon Sep 17 00:00:00 2001 From: Guillermo Alejandro Gallardo Diez Date: Mon, 6 Jan 2025 23:18:24 +0100 Subject: [PATCH 2/3] fix: contract --- contract-rs/Cargo.toml | 6 +++--- frontend/src/components/DonationForm.jsx | 4 +++- frontend/src/pages/_app.js | 6 ------ 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/contract-rs/Cargo.toml b/contract-rs/Cargo.toml index 98be752..eaba807 100644 --- a/contract-rs/Cargo.toml +++ b/contract-rs/Cargo.toml @@ -13,11 +13,11 @@ crate-type = ["cdylib", "rlib"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -near-sdk = { version = "5.1.0", features = ["legacy"] } +near-sdk = { version = "5.7.0", features = ["legacy"] } [dev-dependencies] -near-sdk = { version = "5.0.0", features = ["unit-testing"] } -near-workspaces = { version = "0.10.0", features = ["unstable"] } +near-sdk = { version = "5.7.0", features = ["unit-testing"] } +near-workspaces = { version = "0.16.0", features = ["unstable"] } tokio = { version = "1.12.0", features = ["full"] } serde_json = "1" diff --git a/frontend/src/components/DonationForm.jsx b/frontend/src/components/DonationForm.jsx index 0f3aab9..c972e18 100644 --- a/frontend/src/components/DonationForm.jsx +++ b/frontend/src/components/DonationForm.jsx @@ -22,7 +22,7 @@ const DonationForm = ({ setMyDonation }) => { event.preventDefault(); let deposit = utils.format.parseNearAmount(amount.toString()); - setMyDonation(amount); + wallet .callMethod({ contractId: DonationNearContract, @@ -32,6 +32,8 @@ const DonationForm = ({ setMyDonation }) => { .catch(() => { setMyDonation(-Number(amount)); }); + + setMyDonation(amount); }; return ( diff --git a/frontend/src/pages/_app.js b/frontend/src/pages/_app.js index 78bd9eb..93b6d18 100644 --- a/frontend/src/pages/_app.js +++ b/frontend/src/pages/_app.js @@ -10,12 +10,6 @@ import { DonationNearContract, NetworkId } from "@/config"; // 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({ -// createAccessKeyFor: DonationNearContract, -// networkId: NetworkId, -// }); - export default function MyApp({ Component, pageProps }) { const [signedAccountId, setSignedAccountId] = useState(""); From 8ab271e105c1084206b2c3c2297ca49bd0fe3d5e Mon Sep 17 00:00:00 2001 From: Guillermo Alejandro Gallardo Diez Date: Mon, 6 Jan 2025 23:27:09 +0100 Subject: [PATCH 3/3] fix: deprecated structure --- contract-rs/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contract-rs/src/lib.rs b/contract-rs/src/lib.rs index 9078613..4bc2cb1 100644 --- a/contract-rs/src/lib.rs +++ b/contract-rs/src/lib.rs @@ -1,5 +1,5 @@ // Find all our documentation at https://docs.near.org -use near_sdk::store::UnorderedMap; +use near_sdk::store::IterableMap; use near_sdk::{near, AccountId, NearToken, PanicOnDefault}; mod donation; @@ -8,7 +8,7 @@ mod donation; #[derive(PanicOnDefault)] pub struct Contract { pub beneficiary: AccountId, - pub donations: UnorderedMap, + pub donations: IterableMap, } #[near] @@ -18,7 +18,7 @@ impl Contract { pub fn init(beneficiary: AccountId) -> Self { Self { beneficiary, - donations: UnorderedMap::new(b"d"), + donations: IterableMap::new(b"d"), } }