diff --git a/app/app.vue b/app/app.vue index 78592f9..c26fc93 100644 --- a/app/app.vue +++ b/app/app.vue @@ -5,7 +5,14 @@ import '@vue-dapp/modal/dist/style.css' // import { WalletConnectConnector } from '@vue-dapp/walletconnect' // import { CoinbaseWalletConnector } from '@vue-dapp/coinbase' -import { lightTheme } from 'naive-ui' +import { darkTheme, lightTheme, type GlobalThemeOverrides } from 'naive-ui' + +const lightThemeOverrides: GlobalThemeOverrides = { + common: { + primaryColor: darkTheme.common.primaryColor, + successColor: darkTheme.common.successColor, + }, +} useHead({ titleTemplate: title => { @@ -49,16 +56,22 @@ onWalletUpdated(async (wallet: ConnWallet) => { onDisconnected(() => { resetWallet() }) + +const hideConnectingModal = computed(() => { + const route = useRoute() + if (route.path === '/eip-6963') return true + return false +}) diff --git a/app/components/button/ConnectButton.vue b/app/components/button/ConnectButton.vue index 384cec8..63a314d 100644 --- a/app/components/button/ConnectButton.vue +++ b/app/components/button/ConnectButton.vue @@ -2,7 +2,7 @@ import { useVueDapp, shortenAddress } from '@vue-dapp/core' import { useVueDappModal } from '@vue-dapp/modal' -const { address, status, error, disconnect } = useVueDapp() +const { address, status, disconnect } = useVueDapp() function onClickConnectButton() { if (status.value === 'connected') { @@ -27,7 +27,6 @@ function onClickConnectButton() {
Disconnect
{{ shortenAddress(address) }}
-
{{ error }}
diff --git a/app/components/content/Contract.vue b/app/components/content/Contract.vue index eaca87d..730f231 100644 --- a/app/components/content/Contract.vue +++ b/app/components/content/Contract.vue @@ -172,7 +172,16 @@ const isReady = computed(() => isConnected.value && !showSwitchButton.value)

Events

-
No event found.
+
+
No event found in the last 40,000 blocks.
+ + More on explorer + +
diff --git a/app/components/content/Eip6963.client.vue b/app/components/content/Eip6963.client.vue new file mode 100644 index 0000000..c16f2ec --- /dev/null +++ b/app/components/content/Eip6963.client.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/app/content/eip-6963.md b/app/content/eip-6963.md index 0b61b36..5b675d1 100644 --- a/app/content/eip-6963.md +++ b/app/content/eip-6963.md @@ -6,7 +6,91 @@ head: content: '' --- -# EIP-6963 +# EIP-6963 Multi Injected Provider Discovery -Decentralize injected wallet providers +Using window events to announce [EIP-1193](https://eips.ethereum.org/EIPS/eip-1193){:target="_blank"} providers instead of `window.ethereum`. + +- [EIP-6963](https://eips.ethereum.org/EIPS/eip-6963){:target="_blank"} + + +## Provider details + + +::Eip6963 +:: + + +## Source code + +```ts [setup script] +import { useVueDapp, shortenAddress, type ConnectorName, RDNS } from '@vue-dapp/core' +import { useVueDappModal } from '@vue-dapp/modal' + +const { providerDetails, wallet, address, status, connectTo, disconnect, error, isConnected } = useVueDapp() + +const providerList = computed(() => { + return providerDetails.value.slice().sort((a, b) => { + if (a.info.rdns === RDNS.rabby) return -1 + if (b.info.rdns === RDNS.rabby) return 1 + if (a.info.rdns === RDNS.metamask) return -1 + if (b.info.rdns === RDNS.metamask) return 1 + return 0 + }) +}) + +async function onClickWallet(connName: ConnectorName, rdns?: RDNS | string) { + useVueDappModal().close() + await connectTo(connName, { rdns }) +} +``` + +```vue [template] + +``` diff --git a/app/content/eips.md b/app/content/eips.md index 5ae10e6..099bcf5 100644 --- a/app/content/eips.md +++ b/app/content/eips.md @@ -8,7 +8,8 @@ head: # EIPs -Collect the EIPs related to Vue Dapp +Collect the EIPs related to DApp development, especially in frontend. + ## EIP-1193 - [EIP-1193: Ethereum Provider JavaScript API](https://eips.ethereum.org/EIPS/eip-1193){:target="_blank"} diff --git a/app/content/overview.md b/app/content/overview.md index 55ca028..5d94e1f 100644 --- a/app/content/overview.md +++ b/app/content/overview.md @@ -13,7 +13,7 @@ head: ## Wallet & isConnected -These two states will be frequently used in dapp development. +These two states will be frequently used in development. The `isConnected` is a [computed](https://vuejs.org/api/reactivity-core.html#computed){:target="_blank"}, and the `wallet` is a [readonly](https://vuejs.org/api/reactivity-core.html#readonly) [reactive](https://vuejs.org/api/reactivity-core.html#reactive){:target="_blank"}. @@ -34,7 +34,7 @@ if(isConnected.value) { } ``` -The wallet comprises 8 properties, each of which can be obtained from the `useVueDapp` as a [computed](https://vuejs.org/api/reactivity-core.html#ref){:target="_blank"}. +The wallet comprises 8 properties, each of which can be obtained from the `useVueDapp` as a [computed](https://vuejs.org/api/reactivity-core.html#computed){:target="_blank"}. ```ts const { error, chainId } = useVueDapp() diff --git a/app/core/sidebar.ts b/app/core/sidebar.ts index 6998c22..9354fc1 100644 --- a/app/core/sidebar.ts +++ b/app/core/sidebar.ts @@ -60,6 +60,28 @@ export const sidebarMenu = [ ), key: '/examples/multicall', }, + { + label: () => + h( + NuxtLink, + { + to: '/examples/meta-transactions', + }, + { default: () => 'Meta Transactions' }, + ), + key: '/examples/meta-transactions', + }, + { + label: () => + h( + NuxtLink, + { + to: '/examples/siwe', + }, + { default: () => 'Sign-In with Ethereum' }, + ), + key: '/examples/siwe', + }, ], }, { diff --git a/app/stores/useEthers.ts b/app/stores/useEthers.ts index 26e3228..b41ebf5 100644 --- a/app/stores/useEthers.ts +++ b/app/stores/useEthers.ts @@ -7,7 +7,7 @@ export const useEthers = defineStore('useEthers', () => { const signer = ref(null) async function setWallet(p: EIP1193Provider) { - provider.value = new ethers.BrowserProvider(p) + provider.value = markRaw(new ethers.BrowserProvider(p)) signer.value = markRaw(await provider.value.getSigner()) } diff --git a/packages/core/src/services/connect.ts b/packages/core/src/services/connect.ts index 214c6f6..914333f 100644 --- a/packages/core/src/services/connect.ts +++ b/packages/core/src/services/connect.ts @@ -4,6 +4,11 @@ import { ConnectOptions, ConnectorName, RDNS } from '../types' import { AutoConnectError, ConnectError, ConnectorNotFoundError } from '../errors' import { normalizeChainId } from '../utils' import { BrowserWalletConnector } from '../browserWalletConnector' +import { + getLastConnectedBrowserWallet, + removeLastConnectedBrowserWallet, + setLastConnectedBrowserWallet, +} from './localStorage' export function useConnect(pinia?: any) { const walletStore = useStore(pinia) @@ -41,15 +46,15 @@ export function useConnect(pinia?: any) { walletStore.wallet.provider = provider walletStore.wallet.address = account walletStore.wallet.chainId = normalizeChainId(chainId) + + walletStore.wallet.status = 'connected' + if (info?.rdns) setLastConnectedBrowserWallet(info.rdns) } catch (err: any) { await disconnect() // will resetWallet() walletStore.wallet.error = err.message throw new ConnectError(err) } - walletStore.wallet.status = 'connected' - localStorage.removeItem('VUE_DAPP__disconnected') - // ============================= listen EIP-1193 events ============================= // Events: disconnect, chainChanged, and accountsChanged @@ -82,46 +87,38 @@ export function useConnect(pinia?: any) { } async function disconnect() { - // console.log('useConnect.disconnect') - if (walletStore.wallet.connector) { - try { + try { + if (walletStore.wallet.connector) { await walletStore.wallet.connector.disconnect() - } catch (err: any) { - resetWallet() - throw new Error(err) } + } catch (err: any) { + walletStore.error = `Failed to disconnect, wallet reset: ${err.message}` + throw new Error(`Failed to disconnect, wallet reset: ${err.message}`) + } finally { + resetWallet() + removeLastConnectedBrowserWallet() } - resetWallet() - - localStorage.setItem('VUE_DAPP__disconnected', 'true') } - async function autoConnect(rdns: RDNS | string) { - if (localStorage.getItem('VUE_DAPP__disconnected')) { - // console.warn('No auto-connect: has disconnected') - return - } + async function autoConnect(rdns?: RDNS | string) { + const lastRdns = getLastConnectedBrowserWallet() + if (!lastRdns) return - const browserWalletConn = walletStore.connectors.find(conn => conn.name === 'BrowserWallet') - - if (browserWalletConn) { - try { - const isConnected = await BrowserWalletConnector.checkConnection(rdns) - if (isConnected) { - await connectTo(browserWalletConn.name, { - rdns, - }) - } else { - // console.warn('No auto-connect to MetaMask: not connected') - } - } catch (err: any) { - throw new AutoConnectError(err) + rdns = rdns || lastRdns + + const bwConnector = walletStore.connectors.find(conn => conn.name === 'BrowserWallet') + + if (!bwConnector || !rdns) return + + try { + const isConnected = await BrowserWalletConnector.checkConnection(rdns) + if (isConnected) { + await connectTo(bwConnector.name, { rdns }) } - } else { - // console.warn('No auto-connect to MetaMask: connector not found') + } catch (err: any) { + throw new AutoConnectError(err) } } - return { wallet: readonly(walletStore.wallet), diff --git a/packages/core/src/services/eip6963.ts b/packages/core/src/services/eip6963.ts index 4141f59..0e347ab 100644 --- a/packages/core/src/services/eip6963.ts +++ b/packages/core/src/services/eip6963.ts @@ -3,35 +3,47 @@ import { EIP6963AnnounceProviderEvent, EIP6963ProviderDetail, RDNS } from '../ty import { useStore } from '../store' export function useEIP6963(pinia?: any) { - const walletStore = useStore(pinia) + const store = useStore(pinia) + + let listener: (event: EIP6963AnnounceProviderEvent) => void function subscribe() { - window.addEventListener('eip6963:announceProvider', (event: EIP6963AnnounceProviderEvent) => { + const listener = (event: EIP6963AnnounceProviderEvent) => { // console.log('eip6963:announceProvider -> detail', event.detail) _addProviderDetail(event.detail) - }) + } + + window.addEventListener('eip6963:announceProvider', listener) window.dispatchEvent(new CustomEvent('eip6963:requestProvider')) + + return () => window.removeEventListener('eip6963:announceProvider', listener) + } + + function removeListener() { + if (!listener) return + window.removeEventListener('eip6963:announceProvider', listener) } function _addProviderDetail(detail: EIP6963ProviderDetail) { - if (walletStore.providerDetails.some(({ info }) => info.uuid === detail.info.uuid)) return - walletStore.providerDetails.push(detail) // why detail cannot be markRaw()? it will lead to "TypeError: Cannot define property __v_skip, object is not extensible" + if (store.providerDetails.some(({ info }) => info.uuid === detail.info.uuid)) return + store.providerDetails.push(detail) // why detail cannot be markRaw()? it will lead to "TypeError: Cannot define property __v_skip, object is not extensible" } function getProviderDetail(rdns: string | RDNS): EIP6963ProviderDetail | undefined { - return walletStore.providerDetails.find(({ info }) => info.rdns === rdns) + return store.providerDetails.find(({ info }) => info.rdns === rdns) } return { // state - providerDetails: computed(() => walletStore.providerDetails), + providerDetails: computed(() => store.providerDetails), // getters hasInjectedProvider: computed(() => typeof window !== 'undefined' && !!window.ethereum), - isProviderAnnounced: computed(() => walletStore.providerDetails.length > 0), + isProviderAnnounced: computed(() => store.providerDetails.length > 0), subscribe, getProviderDetail, + removeListener, } } diff --git a/packages/core/src/services/localStorage.ts b/packages/core/src/services/localStorage.ts new file mode 100644 index 0000000..f799ed1 --- /dev/null +++ b/packages/core/src/services/localStorage.ts @@ -0,0 +1,41 @@ +import { RDNS } from '../types' + +const LS_KEY = 'VUE_DAPP' + +export function setLastConnectedBrowserWallet(rdns: RDNS | string) { + window.localStorage.setItem( + LS_KEY, + JSON.stringify({ + lastConnectedWalletRdns: rdns, + }), + ) +} + +export function removeLastConnectedBrowserWallet() { + const data = window.localStorage.getItem(LS_KEY) + if (!data) return + try { + const { lastConnectedWalletRdns } = JSON.parse(data) + if (lastConnectedWalletRdns) { + window.localStorage.setItem( + LS_KEY, + JSON.stringify({ + lastConnectedWalletRdns: undefined, + }), + ) + } + } catch { + return + } +} + +export function getLastConnectedBrowserWallet(): RDNS | undefined { + const data = window.localStorage.getItem(LS_KEY) + if (!data) return + try { + const { lastConnectedWalletRdns } = JSON.parse(data) + return lastConnectedWalletRdns + } catch { + return + } +} diff --git a/packages/core/src/store.ts b/packages/core/src/store.ts index 56b99ae..6ee491e 100644 --- a/packages/core/src/store.ts +++ b/packages/core/src/store.ts @@ -1,7 +1,13 @@ import { reactive, ref, toRefs } from 'vue' import { defineStore } from 'pinia' -import { Connector, EIP6963ProviderDetail, OnDisconnectCallback } from './types' -import { Wallet, OnAccountsChangedCallback, OnChainChangedCallback } from './types' +import { + Wallet, + Connector, + EIP6963ProviderDetail, + OnDisconnectCallback, + OnAccountsChangedCallback, + OnChainChangedCallback, +} from './types' /** * Pinia Setup Store diff --git a/packages/modal/src/VueDappModal.vue b/packages/modal/src/VueDappModal.vue index cd961d6..8e61cdd 100644 --- a/packages/modal/src/VueDappModal.vue +++ b/packages/modal/src/VueDappModal.vue @@ -11,12 +11,14 @@ const props = withDefaults( dark?: boolean autoConnect?: boolean autoConnectBrowserWalletIfSolo?: boolean + hideConnectingModal?: boolean }>(), { modelValue: undefined, dark: false, autoConnect: false, autoConnectBrowserWalletIfSolo: false, + hideConnectingModal: false, }, ) @@ -44,8 +46,7 @@ async function handleAutoConnect() { if (props.autoConnect) { try { isAutoConnecting.value = true - // throw new Error('test autoConnect error') - await autoConnect(RDNS.metamask) + await autoConnect() } catch (err: any) { emit('autoConnectError', err) } finally { @@ -154,7 +155,7 @@ const vClickOutside = { - +

Connecting...