diff --git a/packages/ethereum-wallets/src/lib/index.ts b/packages/ethereum-wallets/src/lib/index.ts index 0633bf670..9f9951317 100644 --- a/packages/ethereum-wallets/src/lib/index.ts +++ b/packages/ethereum-wallets/src/lib/index.ts @@ -38,7 +38,7 @@ const importWagmiCore = async () => { }; import icon from "./icon"; -import { createModal } from "./modal"; +import { createTxModal, createChainSwitchModal } from "./modal"; import { ETHEREUM_ACCOUNT_ABI, DEFAULT_ACCESS_KEY_ALLOWANCE, @@ -111,15 +111,15 @@ const EthereumWallets: WalletBehaviourFactory< const _state = await setupEthereumWalletsState(id); const expectedChainId = chainId ?? (options.network.networkId === "mainnet" ? 397 : 398); - const nearRpc = wagmiConfig.chains.find( - (chain) => chain.id === expectedChainId - )?.rpcUrls.default.http[0]; + const chain = wagmiConfig.chains.find((c) => c.id === expectedChainId); + if (!chain) { + throw new Error("Failed to parse NEAR chain from wagmiConfig."); + } + const nearRpc = chain.rpcUrls.default.http[0]; if (!nearRpc) { throw new Error("Failed to parse NEAR rpc url from wagmiConfig."); } - const nearExplorer = wagmiConfig.chains.find( - (chain) => chain.id === expectedChainId - )?.blockExplorers?.default.url; + const nearExplorer = chain.blockExplorers?.default.url; if (!nearExplorer) { throw new Error("Failed to parse NEAR explorer url from wagmiConfig."); } @@ -473,6 +473,29 @@ const EthereumWallets: WalletBehaviourFactory< } }; + const switchChain = async () => { + const account = wagmiCore!.getAccount(wagmiConfig); + if (account.chainId !== expectedChainId) { + const { showModal, hideModal } = createChainSwitchModal({ + chain, + }); + showModal(); + try { + await wagmiCore!.switchChain(wagmiConfig, { + chainId: expectedChainId, + }); + } catch (error) { + logger.error(error); + // TODO: add the link to onboarding page when available. + throw new Error( + "Wallet didn't connect to NEAR Protocol network, try adding and selecting the network manually inside wallet settings." + ); + // NOTE: we don't hide the modal in case of error to allow the user to add the network manually. + } + hideModal(); + } + }; + const signAndSendTransactions = async ( transactions: Array> ) => { @@ -529,13 +552,11 @@ const EthereumWallets: WalletBehaviourFactory< // Onboard the relayer before executing other transactions. txs = [onboardingTransaction, ...txs]; } - await wagmiCore!.switchChain(wagmiConfig, { - chainId: expectedChainId, - }); + await switchChain(); const results: Array = []; await (() => { return new Promise((resolve, reject) => { - const { showModal, hideModal, renderTxs } = createModal({ + const { showModal, hideModal, renderTxs } = createTxModal({ onCancel: () => { reject("User canceled Ethereum wallet transaction(s)."); }, @@ -737,14 +758,14 @@ const EthereumWallets: WalletBehaviourFactory< _state.isConnecting = true; let unwatchAccountConnected: (() => void) | undefined; let unsubscribeCloseModal: (() => void) | undefined; - const account = wagmiCore!.getAccount(wagmiConfig); + let account = wagmiCore!.getAccount(wagmiConfig); let address = account.address?.toLowerCase(); // Open web3Modal and wait for a wallet to be connected or for the web3Modal to be closed. if (!address) { try { if (web3Modal) { web3Modal.open(); - const newData: GetAccountReturnType = await (() => { + await (() => { return new Promise((resolve, reject) => { try { unwatchAccountConnected = wagmiCore!.watchAccount( @@ -765,9 +786,6 @@ const EthereumWallets: WalletBehaviourFactory< event.data.event === "MODAL_CLOSE" && !newAccount.address ) { - logger.error( - "Web3Modal closed without connecting to an Ethereum wallet." - ); reject( "Web3Modal closed without connecting to an Ethereum wallet." ); @@ -779,13 +797,13 @@ const EthereumWallets: WalletBehaviourFactory< } }); })(); - address = newData.address?.toLowerCase(); } else { - const { accounts } = await wagmiCore!.connect(wagmiConfig, { + await wagmiCore!.connect(wagmiConfig, { connector: wagmiCore!.injected(), }); - address = accounts[0]?.toLowerCase(); } + account = wagmiCore!.getAccount(wagmiConfig); + address = account.address?.toLowerCase(); if (!address) { throw new Error("Failed to get Ethereum wallet address"); } @@ -809,17 +827,7 @@ const EthereumWallets: WalletBehaviourFactory< logger.log("Wallet already connected"); } - try { - await wagmiCore!.switchChain(wagmiConfig, { - chainId: expectedChainId, - }); - } catch (error) { - logger.error(error); - // TODO: add the link to onboarding page when available. - throw new Error( - "Wallet didn't connect to NEAR Protocol network, try adding and selecting the network manually inside wallet settings." - ); - } + await switchChain(); // Login with FunctionCall access key, reuse keypair or create a new one. const accountId = devMode ? address + "." + devModeAccount : address; diff --git a/packages/ethereum-wallets/src/lib/modal.ts b/packages/ethereum-wallets/src/lib/modal.ts index ae29a51eb..ea8c46067 100644 --- a/packages/ethereum-wallets/src/lib/modal.ts +++ b/packages/ethereum-wallets/src/lib/modal.ts @@ -1,8 +1,10 @@ import type { Transaction } from "@near-wallet-selector/core"; import { formatUnits } from "viem"; +import type { Chain } from "viem"; import { DEFAULT_ACCESS_KEY_ALLOWANCE, RLP_EXECUTE } from "./utils"; +import { modalStyles } from "./styles"; -export function createModal({ +export function createTxModal({ onCancel, txs, relayerPublicKey, @@ -13,342 +15,6 @@ export function createModal({ relayerPublicKey: string; explorerUrl: string; }) { - const modalStyles = ` - .ethereum-wallet-modal *, - .ethereum-wallet-modal *::before, - .ethereum-wallet-modal *::after { - box-sizing: border-box; - } - .ethereum-wallet-modal button { - cursor: pointer; - top: auto; - } - .ethereum-wallet-modal button:hover { - top: auto; - } - .ethereum-wallet-modal button:focus-visible { - top: auto; - } - - .ethereum-wallet-modal { - display: none; - position: relative; - z-index: 9999; - } - .ethereum-wallet-modal-backdrop { - position: fixed; - left: 0; - right: 0; - top: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.5); - } - .ethereum-wallet-modal-wrapper { - position: fixed; - left: 0; - right: 0; - top: 0; - bottom: 0; - width: 100vw; - overflow-y: auto; - } - .ethereum-wallet-modal-container { - display: flex; - min-height: 100%; - align-items: center; - justify-content: center; - padding: 1rem; - text-align: center; - } - .ethereum-wallet-modal-content { - position: relative; - overflow: hidden; - border-radius: 1rem; - background-color: #fff; - padding: 20px; - text-align: left; - max-width: 400px; - width: 100%; - box-shadow: - 0px 4px 8px rgba(0, 0, 0, 0.06), - 0px 0px 0px 1px rgba(0, 0, 0, 0.06); - } - - .ethereum-wallet-modal h2 { - font-weight: 700; - font-size: 24px; - line-height: 30px; - text-align: center; - letter-spacing: -0.1px; - color: #202020; - margin: 0; - } - - .ethereum-wallet-txs { - margin: 20px 0 10px 0; - display: flex; - flex-direction: column; - row-gap: 8px; - } - - .ethereum-wallet-tx { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - } - .ethereum-wallet-tx:not(.ethereum-wallet-tx-single) { - padding: 0 10px; - } - .ethereum-wallet-tx-list-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 12px 10px; - width: 100%; - } - .ethereum-wallet-tx-list-header p { - margin: 0; - font-size: 14px; - line-height: 20px; - color: #202020; - font-weight: 700; - } - .ethereum-wallet-tx-list-header svg { - height: 24px; - width: 24px; - } - - .ethereum-wallet-tx-explorer-link { - height: 24px; - width: 24px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 6px; - } - .ethereum-wallet-tx-explorer-link:hover { - background-color: #DDF3E4; - } - .ethereum-wallet-tx-explorer-link svg { - height: 16px; - width: 16px; - color: #202020; - } - - .ethereum-wallet-tx.ethereum-wallet-tx-signing { - background-color: #F9F9F9; - padding-bottom: 10px; - border-radius: 8px; - } - .ethereum-wallet-tx.ethereum-wallet-tx-pending { - background-color: #F9F9F9; - border-radius: 8px; - } - .ethereum-wallet-tx.ethereum-wallet-tx-completed { - background-color: #F2FCF5; - border-radius: 8px; - } - - .ethereum-wallet-tx-single .ethereum-wallet-tx-info-container { - border-top: 1px solid #EBEBEB; - border-bottom: 1px solid #EBEBEB; - } - .ethereum-wallet-tx-info-text > p { - color: #646464; - margin: 0; - font-weight: 500; - font-size: 12px; - line-height: 16px; - letter-spacing: 0.04px; - } - .ethereum-wallet-tx-info-text > p:not(:last-child) { - margin-bottom: 8px; - } - - .ethereum-wallet-tx-info-row { - padding: 14px 10px; - display: flex; - flex-direction: row; - justify-content: space-between; - column-gap: 20px; - } - .ethereum-wallet-tx-info-text { - padding: 10px; - } - .ethereum-wallet-tx-params, - .ethereum-wallet-tx-params dl { - margin: 0; - } - .ethereum-wallet-tx .ethereum-wallet-tx-params { - border-top: 1px solid #EBEBEB; - border-bottom: 1px solid #EBEBEB; - } - .ethereum-wallet-tx-params div:not(:last-child) { - border-bottom: 1px solid #EBEBEB; - } - .ethereum-wallet-tx-params dt { - margin: 0; - font-size: 14px; - line-height: 20px; - color: #202020; - font-weight: 500; - } - .ethereum-wallet-tx-params dd { - margin: 0; - font-size: 14px; - line-height: 20px; - color: #202020; - font-weight: 700; - text-align: right; - word-break: break-all; - overflow-wrap: break-word; - } - - .ethereum-wallet-btn { - display: flex; - align-items: center; - justify-content: center; - border-radius: 8px; - color: #1C2024; - border: 1px solid rgba(1, 6, 47, 0.173) !important; - background-color: #fff !important; - padding: 14px; - font-size: 14px; - line-height: 20px; - font-weight: 700; - } - .ethereum-wallet-btn:hover { - border: 1px solid rgba(1, 6, 47, 0.173); - background-color: #F2F2F5 !important; - } - .ethereum-wallet-btn:focus-visible { - outline: 2px solid; - outline-offset: 2px; - outline-color: #8B8D98; - border: 1px solid rgba(1, 6, 47, 0.173); - } - .ethereum-wallet-btn-sm { - padding: 10px 14px; - font-size: 12px; - line-height: 16px; - } - .ethereum-wallet-btn-xs { - padding: 8px 12px; - font-weight: 500; - font-size: 12px; - line-height: 16px; - letter-spacing: 0.04px; - border-radius: 9999px; - } - .ethereum-wallet-btn-confirm { - width: 100%; - margin-top: 24px; - } - .ethereum-wallet-btn-cancel { - width: 100%; - background-color: #FFF9F9 !important; - color: #dc2626 !important; - border: 1px solid #fecaca !important; - } - .ethereum-wallet-btn-cancel:hover { - background-color: #fef2f2 !important; - } - .ethereum-wallet-btn-details { - margin-top: 20px; - border-radius: 9999px; - } - - .ethereum-wallet-tx-error { - font-size: 14px; - line-height: 20px; - color: #dc2626; - font-weight: 700; - text-align: center; - text-wrap: balance; - margin-top: 20px; - margin-bottom: 0px; - } - - .ethereum-wallet-txs-details { - display: none; - margin-top: 10px; - padding: 10px; - background: #F1F1F1; - border-radius: 8px; - width: 100%; - max-width: 100%; - overflow: auto; - } - .ethereum-wallet-txs-details p { - font-weight: 500; - font-size: 12px; - line-height: 16px; - letter-spacing: 0.04px; - color: #646464; - word-wrap: break-word; - overflow-wrap: break-word; - white-space: pre-wrap; - margin: 0; - } - - .ethereum-wallet-txs-status { - position: relative; - display: flex; - justify-content: center; - align-items: center; - padding: 14px; - background: #F1F4FE; - border-radius: 8px; - width: 100%; - margin-top: 24px; - border: 1px solid rgba(0,0,0,0); - } - .ethereum-wallet-txs-status p { - margin: 0; - color: #384EAC; - font-weight: 700; - font-size: 14px; - line-height: 20px; - } - - .ethereum-wallet-tx-highlight { - position: relative; - z-index: 1; - } - .ethereum-wallet-tx-highlight::before { - content: ""; - position: absolute; - top: -4px; - left: -6px; - right: -6px; - bottom: -4px; - z-index: -1; - background-color: #DDF3E4; - border-radius: 8px; - } - - .ethereum-wallet-spinner { - position: absolute; - right: 14px; - top: 16px; - width: 16px; - height: 16px; - border: 2px solid #384EAC; - border-bottom-color: transparent; - border-radius: 50%; - animation: rotation 1s linear infinite; - } - @keyframes rotation { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } - } - `; - // Create a style element and append the CSS styles const styleElement = window.document.createElement("style"); styleElement.textContent = modalStyles; @@ -692,3 +358,94 @@ export function createModal({ }; return { showModal, hideModal, renderTxs }; } + +export function createChainSwitchModal({ chain }: { chain: Chain }) { + // Create a style element and append the CSS styles + const styleElement = window.document.createElement("style"); + styleElement.textContent = modalStyles; + window.document.head.appendChild(styleElement); + + // Container with display none/block + const modalContainer = window.document.createElement("div"); + modalContainer.classList.add("ethereum-wallet-modal"); + modalContainer.setAttribute("aria-labelledby", "modal-title"); + modalContainer.setAttribute("role", "dialog"); + modalContainer.setAttribute("aria-modal", "true"); + + // Backdrop + const backdrop = window.document.createElement("div"); + backdrop.classList.add("ethereum-wallet-modal-backdrop"); + + // Wrapper for modal + const modalWrapper = window.document.createElement("div"); + modalWrapper.classList.add("ethereum-wallet-modal-wrapper"); + + // Modal content container + const modalContentContainer = window.document.createElement("div"); + modalContentContainer.classList.add("ethereum-wallet-modal-container"); + + // Modal content + const modalContent = window.document.createElement("div"); + modalContent.classList.add("ethereum-wallet-modal-content"); + modalContent.innerHTML = ` +

Switch Network

+
+
+

Please approve the switch network request in your wallet.

+

If you experience problems connecting you may need to add the network manually from your wallet settings and try again.

+
+
+
+
Network Name
+
${chain.name}
+
+
+
RPC URL
+
${chain.rpcUrls.default.http[0]}
+
+
+
Chain ID
+
${chain.id}
+
+
+
Symbol
+
${chain.nativeCurrency.symbol}
+
+
+
Block Explorer URL
+
${chain.blockExplorers?.default.url}
+
+
+
+ + `; + + // // Append the elements to form the complete structure + modalContentContainer.appendChild(modalContent); + modalWrapper.appendChild(modalContentContainer); + modalContainer.appendChild(backdrop); + modalContainer.appendChild(modalWrapper); + + // Append modal container to document body + window.document.body.appendChild(modalContainer); + + // Function to show the modal + const showModal = () => { + modalContainer.style.display = "block"; + }; + + // Function to hide the modal + const hideModal = () => { + // modalContainer.style.display = "none"; + modalContainer.remove(); + }; + + // On cancel button click + window.document + .querySelector(".ethereum-wallet-btn-cancel") + ?.addEventListener("click", () => { + hideModal(); + }); + + return { showModal, hideModal }; +} diff --git a/packages/ethereum-wallets/src/lib/styles.ts b/packages/ethereum-wallets/src/lib/styles.ts new file mode 100644 index 000000000..91fc762e1 --- /dev/null +++ b/packages/ethereum-wallets/src/lib/styles.ts @@ -0,0 +1,345 @@ +export const modalStyles = ` + .ethereum-wallet-modal *, + .ethereum-wallet-modal *::before, + .ethereum-wallet-modal *::after { + box-sizing: border-box; + } + .ethereum-wallet-modal button { + cursor: pointer; + top: auto; + } + .ethereum-wallet-modal button:hover { + top: auto; + } + .ethereum-wallet-modal button:focus-visible { + top: auto; + } + + .ethereum-wallet-modal dt { + flex-shrink: 0; + margin: 0; + font-size: 14px; + line-height: 20px; + color: #202020; + font-weight: 500; + } + .ethereum-wallet-modal dd { + margin: 0; + font-size: 14px; + line-height: 20px; + color: #202020; + font-weight: 700; + text-align: right; + word-break: break-all; + overflow-wrap: break-word; + } + + .ethereum-wallet-modal { + display: none; + position: relative; + z-index: 9999; + } + .ethereum-wallet-modal-backdrop { + position: fixed; + left: 0; + right: 0; + top: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + } + .ethereum-wallet-modal-wrapper { + position: fixed; + left: 0; + right: 0; + top: 0; + bottom: 0; + width: 100vw; + overflow-y: auto; + } + .ethereum-wallet-modal-container { + display: flex; + min-height: 100%; + align-items: center; + justify-content: center; + padding: 1rem; + text-align: center; + } + .ethereum-wallet-modal-content { + position: relative; + overflow: hidden; + border-radius: 1rem; + background-color: #fff; + padding: 20px; + text-align: left; + max-width: 400px; + width: 100%; + box-shadow: + 0px 4px 8px rgba(0, 0, 0, 0.06), + 0px 0px 0px 1px rgba(0, 0, 0, 0.06); + } + + .ethereum-wallet-modal h2 { + font-weight: 700; + font-size: 24px; + line-height: 30px; + text-align: center; + letter-spacing: -0.1px; + color: #202020; + margin: 0; + } + + .ethereum-wallet-txs { + margin: 20px 0 10px 0; + display: flex; + flex-direction: column; + row-gap: 8px; + } + + .ethereum-wallet-tx { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + } + .ethereum-wallet-tx:not(.ethereum-wallet-tx-single) { + padding: 0 10px; + } + .ethereum-wallet-tx-list-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 10px; + width: 100%; + } + .ethereum-wallet-tx-list-header p { + margin: 0; + font-size: 14px; + line-height: 20px; + color: #202020; + font-weight: 700; + } + .ethereum-wallet-tx-list-header svg { + height: 24px; + width: 24px; + } + + .ethereum-wallet-tx-explorer-link { + height: 24px; + width: 24px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 6px; + } + .ethereum-wallet-tx-explorer-link:hover { + background-color: #DDF3E4; + } + .ethereum-wallet-tx-explorer-link svg { + height: 16px; + width: 16px; + color: #202020; + } + + .ethereum-wallet-tx.ethereum-wallet-tx-signing { + background-color: #F9F9F9; + padding-bottom: 10px; + border-radius: 8px; + } + .ethereum-wallet-tx.ethereum-wallet-tx-pending { + background-color: #F9F9F9; + border-radius: 8px; + } + .ethereum-wallet-tx.ethereum-wallet-tx-completed { + background-color: #F2FCF5; + border-radius: 8px; + } + + .ethereum-wallet-tx-single .ethereum-wallet-tx-info-container { + border-top: 1px solid #EBEBEB; + border-bottom: 1px solid #EBEBEB; + } + .ethereum-wallet-tx-info-text > p { + color: #646464; + margin: 0; + font-weight: 500; + font-size: 12px; + line-height: 16px; + letter-spacing: 0.04px; + } + .ethereum-wallet-tx-info-text > p:not(:last-child) { + margin-bottom: 8px; + } + + .ethereum-wallet-tx-info-row { + padding: 14px 10px; + display: flex; + flex-direction: row; + justify-content: space-between; + column-gap: 20px; + } + .ethereum-wallet-tx-info-col { + padding: 14px 10px; + display: flex; + flex-direction: column; + justify-content: start; + row-gap: 4px; + align-items: start; + } + .ethereum-wallet-tx-info-text { + padding: 10px; + } + .ethereum-wallet-tx-params, + .ethereum-wallet-tx-params dl { + margin: 0; + } + .ethereum-wallet-tx .ethereum-wallet-tx-params { + border-top: 1px solid #EBEBEB; + border-bottom: 1px solid #EBEBEB; + } + .ethereum-wallet-tx-params div:not(:last-child) { + border-bottom: 1px solid #EBEBEB; + } + + .ethereum-wallet-btn { + display: flex; + align-items: center; + justify-content: center; + border-radius: 8px; + color: #1C2024; + border: 1px solid rgba(1, 6, 47, 0.173) !important; + background-color: #fff !important; + padding: 14px; + font-size: 14px; + line-height: 20px; + font-weight: 700; + } + .ethereum-wallet-btn:hover { + border: 1px solid rgba(1, 6, 47, 0.173); + background-color: #F2F2F5 !important; + } + .ethereum-wallet-btn:focus-visible { + outline: 2px solid; + outline-offset: 2px; + outline-color: #8B8D98; + border: 1px solid rgba(1, 6, 47, 0.173); + } + .ethereum-wallet-btn-sm { + padding: 10px 14px; + font-size: 12px; + line-height: 16px; + } + .ethereum-wallet-btn-xs { + padding: 8px 12px; + font-weight: 500; + font-size: 12px; + line-height: 16px; + letter-spacing: 0.04px; + border-radius: 9999px; + } + .ethereum-wallet-btn-confirm { + width: 100%; + margin-top: 24px; + } + .ethereum-wallet-btn-cancel { + width: 100%; + background-color: #FFF9F9 !important; + color: #dc2626 !important; + border: 1px solid #fecaca !important; + } + .ethereum-wallet-btn-cancel:hover { + background-color: #fef2f2 !important; + } + .ethereum-wallet-btn-details { + margin-top: 20px; + border-radius: 9999px; + } + + .ethereum-wallet-tx-error { + font-size: 14px; + line-height: 20px; + color: #dc2626; + font-weight: 700; + text-align: center; + text-wrap: balance; + margin-top: 20px; + margin-bottom: 0px; + } + + .ethereum-wallet-txs-details { + display: none; + margin-top: 10px; + padding: 10px; + background: #F1F1F1; + border-radius: 8px; + width: 100%; + max-width: 100%; + overflow: auto; + } + .ethereum-wallet-txs-details p { + font-weight: 500; + font-size: 12px; + line-height: 16px; + letter-spacing: 0.04px; + color: #646464; + word-wrap: break-word; + overflow-wrap: break-word; + white-space: pre-wrap; + margin: 0; + } + + .ethereum-wallet-txs-status { + position: relative; + display: flex; + justify-content: center; + align-items: center; + padding: 14px; + background: #F1F4FE; + border-radius: 8px; + width: 100%; + margin-top: 24px; + border: 1px solid rgba(0,0,0,0); + } + .ethereum-wallet-txs-status p { + margin: 0; + color: #384EAC; + font-weight: 700; + font-size: 14px; + line-height: 20px; + } + + .ethereum-wallet-tx-highlight { + position: relative; + z-index: 1; + } + .ethereum-wallet-tx-highlight::before { + content: ""; + position: absolute; + top: -4px; + left: -6px; + right: -6px; + bottom: -4px; + z-index: -1; + background-color: #DDF3E4; + border-radius: 8px; + } + + .ethereum-wallet-spinner { + position: absolute; + right: 14px; + top: 16px; + width: 16px; + height: 16px; + border: 2px solid #384EAC; + border-bottom-color: transparent; + border-radius: 50%; + animation: rotation 1s linear infinite; + } + @keyframes rotation { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } +`;