diff --git a/packages/@magic-ext/web3modal-ethers/.eslintignore b/packages/@magic-ext/web3modal-ethers/.eslintignore new file mode 100644 index 000000000..5807c501b --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers/.eslintignore @@ -0,0 +1,5 @@ +/node_modules +/coverage +/dist +/.eslintrc.js +/jest.config.ts diff --git a/packages/@magic-ext/web3modal-ethers/.eslintrc.js b/packages/@magic-ext/web3modal-ethers/.eslintrc.js new file mode 100644 index 000000000..a0cd6b34b --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['../../../.eslintrc.js'], + parserOptions: { + project: ['./tsconfig.json', './test/tsconfig.json'], + tsconfigRootDir: __dirname, + }, +}; diff --git a/packages/@magic-ext/web3modal-ethers/.lintstagedrc.yml b/packages/@magic-ext/web3modal-ethers/.lintstagedrc.yml new file mode 100644 index 000000000..1c250ad65 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers/.lintstagedrc.yml @@ -0,0 +1,2 @@ +'*.{ts,tsx}': + - eslint --fix diff --git a/packages/@magic-ext/web3modal-ethers/.prettierrc.js b/packages/@magic-ext/web3modal-ethers/.prettierrc.js new file mode 100644 index 000000000..6177cac66 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers/.prettierrc.js @@ -0,0 +1 @@ +module.exports = require('../../../.prettierrc.js'); diff --git a/packages/@magic-ext/web3modal-ethers/CHANGELOG.md b/packages/@magic-ext/web3modal-ethers/CHANGELOG.md new file mode 100644 index 000000000..e69de29bb diff --git a/packages/@magic-ext/web3modal-ethers/LICENSE b/packages/@magic-ext/web3modal-ethers/LICENSE new file mode 100644 index 000000000..7335bc897 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers/LICENSE @@ -0,0 +1,177 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/packages/@magic-ext/web3modal-ethers/babel.config.json b/packages/@magic-ext/web3modal-ethers/babel.config.json new file mode 100644 index 000000000..7521eb073 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers/babel.config.json @@ -0,0 +1,7 @@ +{ + "env": { + "test": { + "plugins": ["@babel/plugin-transform-modules-commonjs"] + } + } +} diff --git a/packages/@magic-ext/web3modal-ethers/jest.config.ts b/packages/@magic-ext/web3modal-ethers/jest.config.ts new file mode 100644 index 000000000..a25ade0ed --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers/jest.config.ts @@ -0,0 +1,13 @@ +import baseJestConfig from '../../../jest.config'; +import type { Config } from '@jest/types'; + +const config: Config.InitialOptions = { + ...baseJestConfig, + transform: { + '^.+\\.(js|jsx)$': 'babel-jest', + '\\.(ts|tsx)$': 'ts-jest', + }, + coveragePathIgnorePatterns: ['index.ts', 'index.cdn.ts', 'index.native.ts'], +}; + +export default config; diff --git a/packages/@magic-ext/web3modal-ethers/package.json b/packages/@magic-ext/web3modal-ethers/package.json new file mode 100644 index 000000000..b11e5292e --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers/package.json @@ -0,0 +1,38 @@ +{ + "name": "@magic-ext/web3modal-ethers", + "version": "0.1.0", + "description": "magic web3modal ethers extension", + "author": "Magic (https://magic.link/)", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/magiclabs/magic-js" + }, + "files": [ + "dist" + ], + "target": "neutral", + "cdnGlobalName": "MagicWeb3ModalExtension", + "main": "./dist/cjs/index.js", + "module": "./dist/es/index.js", + "types": "./dist/types/index.d.ts", + "jsdelivr": "./dist/extension.js", + "exports": { + "import": "./dist/es/index.mjs", + "types": "./dist/types/index.d.ts", + "require": "./dist/cjs/index.js" + }, + "externals": { + "include": [ + "@magic-sdk/commons" + ] + }, + "devDependencies": { + "@magic-sdk/commons": "^24.0.2", + "@magic-sdk/types": "24.0.2-canary.742.9175925480.0" + }, + "dependencies": { + "@web3modal/ethers": "^5.0.6", + "ethers": "^6.13.1" + } +} diff --git a/packages/@magic-ext/web3modal-ethers/src/index.cdn.ts b/packages/@magic-ext/web3modal-ethers/src/index.cdn.ts new file mode 100644 index 000000000..af28dc598 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers/src/index.cdn.ts @@ -0,0 +1,3 @@ +import { Web3ModalExtension } from './index'; + +export default Web3ModalExtension; diff --git a/packages/@magic-ext/web3modal-ethers/src/index.native.ts b/packages/@magic-ext/web3modal-ethers/src/index.native.ts new file mode 100644 index 000000000..ea465c2a3 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers/src/index.native.ts @@ -0,0 +1 @@ +export * from './index'; diff --git a/packages/@magic-ext/web3modal-ethers/src/index.ts b/packages/@magic-ext/web3modal-ethers/src/index.ts new file mode 100644 index 000000000..97831ea7c --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers/src/index.ts @@ -0,0 +1,121 @@ +import { Extension } from '@magic-sdk/commons'; +import { Web3Modal, createWeb3Modal, defaultConfig } from '@web3modal/ethers'; +import { ThirdPartyWalletEvents } from '@magic-sdk/types'; +import { Web3ModalExtensionOptions } from './types'; + +export class Web3ModalExtension extends Extension.Internal<'web3modal'> { + name = 'web3modal' as const; + config = {}; + modal: Web3Modal; + + static eventsListenerAdded = false; + + constructor({ configOptions, modalOptions }: Web3ModalExtensionOptions) { + super(); + + this.modal = createWeb3Modal({ + ...modalOptions, + ...{ themeVariables: { ...(modalOptions.themeVariables || {}), '--w3m-z-index': 3000000000 } }, + ethersConfig: defaultConfig(configOptions), + }); + + const unsubscribeFromProviderEvents = this.modal.subscribeProvider(({ status }) => { + if (status === 'connected') { + unsubscribeFromProviderEvents(); + this.setIsConnected(); + this.setEip1193EventListeners(); + } + if (status === 'disconnected') { + unsubscribeFromProviderEvents(); + } + }); + } + + public setIsConnected() { + localStorage.setItem('magic_3pw_provider', 'web3modal'); + localStorage.setItem('magic_3pw_address', this.modal.getAddress() as string); + localStorage.setItem('magic_3pw_chainId', (this.modal.getChainId() as number).toString()); + this.sdk.thirdPartyWallets.isConnected = true; + } + + public initialize() { + this.sdk.thirdPartyWallets.enabledWallets.web3modal = true; + this.sdk.thirdPartyWallets.isConnected = Boolean(localStorage.getItem('magic_3pw_address')); + this.sdk.thirdPartyWallets.eventListeners.push({ + event: ThirdPartyWalletEvents.Web3ModalSelected, + callback: async (payloadId) => { + await this.connectToWeb3modal(payloadId); + }, + }); + } + + private setEip1193EventListeners() { + if (Web3ModalExtension.eventsListenerAdded) return; + Web3ModalExtension.eventsListenerAdded = true; + + this.modal.subscribeProvider(({ address, chainId }) => { + // If user disconnected all accounts from wallet + if (!address && localStorage.getItem('magic_3pw_address')) { + this.sdk.thirdPartyWallets.resetThirdPartyWalletState(); + return this.sdk.rpcProvider.emit('accountsChanged', []); + } + if (address && address !== localStorage.getItem('magic_3pw_address')) { + localStorage.setItem('magic_3pw_address', address); + return this.sdk.rpcProvider.emit('accountsChanged', [address]); + } + if (chainId && chainId !== Number(localStorage.getItem('magic_3pw_chainId'))) { + localStorage.setItem('magic_3pw_chainId', chainId.toString()); + return this.sdk.rpcProvider.emit('chainChanged', chainId); + } + return null; + }); + } + + private handleUserConnected(payloadId: string, address: string = this.modal.getAddress() as string) { + this.setIsConnected(); + this.createIntermediaryEvent(ThirdPartyWalletEvents.WalletConnected, payloadId)(address); + this.setEip1193EventListeners(); + } + + private connectToWeb3modal(payloadId: string) { + const { modal } = this; + + const promiEvent = this.utils.createPromiEvent(async () => { + try { + if (modal.getIsConnected()) { + await modal.disconnect(); + } + } catch (error) { + console.error(error); + } + + // Listen for wallet connected event + const unsubscribeFromProviderEvents = modal.subscribeProvider(({ address, error }) => { + // User rejected connection request + if (error) { + console.error('Provider event error:', error); + unsubscribeFromProviderEvents(); + this.createIntermediaryEvent(ThirdPartyWalletEvents.WalletRejected, payloadId)(); + } + // If user connected wallet, keep listeners active + if (address) { + this.handleUserConnected(payloadId); + unsubscribeFromProviderEvents(); + } + }); + + // Listen for modal close before user connects wallet + const unsubscribeFromModalEvents = modal.subscribeEvents((event) => { + if (event.data.event === 'MODAL_CLOSE') { + unsubscribeFromModalEvents(); + unsubscribeFromProviderEvents(); + this.createIntermediaryEvent(ThirdPartyWalletEvents.WalletRejected, payloadId)(); + } + }); + + modal.open(); + }); + + return promiEvent; + } +} diff --git a/packages/@magic-ext/web3modal-ethers/src/types.ts b/packages/@magic-ext/web3modal-ethers/src/types.ts new file mode 100644 index 000000000..be47c024e --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers/src/types.ts @@ -0,0 +1,7 @@ +// @ts-expect-error Module '"@web3modal/ethers"' has no exported member 'ConfigOptions'. +import { ConfigOptions, Web3ModalOptions } from '@web3modal/ethers'; + +export interface Web3ModalExtensionOptions { + configOptions: ConfigOptions; + modalOptions: Web3ModalOptions; +} diff --git a/packages/@magic-ext/web3modal-ethers/test/setup.ts b/packages/@magic-ext/web3modal-ethers/test/setup.ts new file mode 100644 index 000000000..cb8bbad06 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers/test/setup.ts @@ -0,0 +1,4 @@ +// NOTE: This module is automatically included at the top of each test file. +import browserEnv from '@ikscodes/browser-env'; + +browserEnv(); diff --git a/packages/@magic-ext/web3modal-ethers/test/spec/accountsChanged.spec.ts b/packages/@magic-ext/web3modal-ethers/test/spec/accountsChanged.spec.ts new file mode 100644 index 000000000..dc47ab1b3 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers/test/spec/accountsChanged.spec.ts @@ -0,0 +1,43 @@ +import browserEnv from '@ikscodes/browser-env'; +import { Web3ModalExtension } from '../../src/index'; +import { createMagicSDKWithExtension } from '../../../../@magic-sdk/provider/test/factories'; +import { mockLocalStorage } from '../../../../@magic-sdk/provider/test/mocks'; + +jest.mock('@web3modal/ethers5', () => ({ + Web3Modal: jest.fn(), + defaultConfig: jest.fn(), + createWeb3Modal: jest.fn(() => { + return { + getIsConnected: jest.fn(), + getAddress: jest.fn(() => '0x123'), + getChainId: jest.fn(() => 1), + subscribeProvider: jest.fn(), + }; + }), +})); + +beforeEach(() => { + browserEnv.restore(); + mockLocalStorage(); +}); + +const web3modalParams = { + configOptions: {}, + modalOptions: { + projectId: '123', + chains: [], + ethersConfig: { metadata: { name: 'test', description: 'test', url: 'test', icons: [] } }, + }, +}; + +test('setEip1193EventListeners emits accountsChanged', () => { + const magic = createMagicSDKWithExtension({}, [new Web3ModalExtension(web3modalParams)]); + magic.web3modal.modal.subscribeProvider = jest.fn( + (callback: (provider: { address: string; chainId: number }) => void) => { + callback({ address: '0x123', chainId: 1 }); + }, + ); + const emitSpy = jest.spyOn(magic.rpcProvider, 'emit').mockImplementation(() => Promise.resolve({})); + magic.web3modal.setEip1193EventListeners(); + expect(emitSpy).toBeCalledWith('accountsChanged', ['0x123']); +}); diff --git a/packages/@magic-ext/web3modal-ethers/test/spec/chainChanged.spec.ts b/packages/@magic-ext/web3modal-ethers/test/spec/chainChanged.spec.ts new file mode 100644 index 000000000..3f4dadaa6 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers/test/spec/chainChanged.spec.ts @@ -0,0 +1,45 @@ +import browserEnv from '@ikscodes/browser-env'; +import { Web3ModalExtension } from '../../src/index'; +import { createMagicSDKWithExtension } from '../../../../@magic-sdk/provider/test/factories'; +import { mockLocalStorage } from '../../../../@magic-sdk/provider/test/mocks'; + +jest.mock('@web3modal/ethers5', () => ({ + Web3Modal: jest.fn(), + defaultConfig: jest.fn(), + createWeb3Modal: jest.fn(() => { + return { + getIsConnected: jest.fn(), + getAddress: jest.fn(() => '0x123'), + getChainId: jest.fn(() => 1), + subscribeProvider: jest.fn(), + }; + }), +})); + +beforeEach(() => { + browserEnv.restore(); + mockLocalStorage(); +}); + +const web3modalParams = { + configOptions: {}, + modalOptions: { + projectId: '123', + chains: [], + ethersConfig: { metadata: { name: 'test', description: 'test', url: 'test', icons: [] } }, + }, +}; + +test('setEip1193EventListeners emits chainChanged', () => { + const magic = createMagicSDKWithExtension({}, [new Web3ModalExtension(web3modalParams)]); + localStorage.setItem('magic_3pw_address', '0x123'); + magic.web3modal.modal.subscribeProvider = jest.fn( + (callback: (provider: { address: string; chainId: number }) => void) => { + callback({ address: '0x123', chainId: 1 }); + }, + ); + const emitSpy = jest.spyOn(magic.rpcProvider, 'emit').mockImplementation(() => Promise.resolve({})); + magic.web3modal.setEip1193EventListeners(); + expect(magic.web3modal.modal.subscribeProvider).toBeCalled(); + expect(emitSpy).toBeCalledWith('chainChanged', 1); +}); diff --git a/packages/@magic-ext/web3modal-ethers/test/spec/connectToWeb3Modal.spec.ts b/packages/@magic-ext/web3modal-ethers/test/spec/connectToWeb3Modal.spec.ts new file mode 100644 index 000000000..4482c83c5 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers/test/spec/connectToWeb3Modal.spec.ts @@ -0,0 +1,111 @@ +import browserEnv from '@ikscodes/browser-env'; +import { Web3ModalExtension } from '../../src/index'; +import { createMagicSDKWithExtension } from '../../../../@magic-sdk/provider/test/factories'; +import { mockLocalStorage } from '../../../../@magic-sdk/provider/test/mocks'; +import { isPromiEvent } from '../../../../@magic-sdk/commons'; + +jest.mock('@web3modal/ethers5', () => ({ + Web3Modal: jest.fn(), + defaultConfig: jest.fn(), + createWeb3Modal: jest.fn(() => { + return { + getIsConnected: jest.fn(), + getAddress: jest.fn(() => '0x123'), + getChainId: jest.fn(() => 1), + subscribeProvider: jest.fn(), + subscribeEvents: jest.fn(), + open: jest.fn(), + }; + }), +})); + +beforeEach(() => { + browserEnv.restore(); + mockLocalStorage(); +}); + +const web3modalParams = { + configOptions: {}, + modalOptions: { + projectId: '123', + chains: [], + ethersConfig: { metadata: { name: 'test', description: 'test', url: 'test', icons: [] } }, + }, +}; + +test('connectToWeb3modal returns promiEvent', () => { + const magic = createMagicSDKWithExtension({}, [new Web3ModalExtension(web3modalParams)]); + expect(isPromiEvent(magic.web3modal.connectToWeb3modal())).toBeTruthy(); +}); + +test('connectToWeb3modal calls subscribeProvider', () => { + const magic = createMagicSDKWithExtension({}, [new Web3ModalExtension(web3modalParams)]); + magic.web3modal.connectToWeb3modal(); + expect(magic.web3modal.modal.subscribeProvider).toBeCalled(); +}); + +test('connectToWeb3modal calls subscribeEvents', () => { + const magic = createMagicSDKWithExtension({}, [new Web3ModalExtension(web3modalParams)]); + magic.web3modal.connectToWeb3modal(); + expect(magic.web3modal.modal.subscribeEvents).toBeCalled(); +}); + +test('connectToWeb3modal calls `open`', () => { + const magic = createMagicSDKWithExtension({}, [new Web3ModalExtension(web3modalParams)]); + magic.web3modal.connectToWeb3modal(); + expect(magic.web3modal.modal.open).toBeCalled(); +}); + +// skip because it does not like calling the unsubscribe function +test.skip('connectToWeb3modal emits wallet_rejected event on subscribeProvider error', () => { + const magic = createMagicSDKWithExtension({}, [new Web3ModalExtension(web3modalParams)]); + magic.web3modal.modal.subscribeProvider = jest.fn((callback: (provider: { error: boolean }) => void) => { + callback({ error: true }); + }); + const createIntermediaryEventFn = jest.fn(); + magic.web3modal.createIntermediaryEvent = jest.fn().mockImplementation(() => createIntermediaryEventFn); + magic.web3modal.connectToWeb3modal(); + const rejectedEvent = magic.web3modal.createIntermediaryEvent.mock.calls[0]; + expect(rejectedEvent[0]).toBe('wallet_rejected'); +}); + +// skip because it does not like calling the unsubscribe function +test.skip('connectToWeb3modal emits wallet_connected event on `address` event', () => { + const magic = createMagicSDKWithExtension({}, [new Web3ModalExtension(web3modalParams)]); + magic.web3modal.modal.subscribeProvider = jest.fn((callback: (provider: { address: string }) => void) => { + callback({ address: '0x123' }); + }); + const setIsConnectedSpy = jest.spyOn(magic.web3modal, 'setIsConnected').mockImplementation(() => Promise.resolve({})); + const setEip1193EventListenersSpy = jest + .spyOn(magic.web3modal, 'setEip1193EventListeners') + .mockImplementation(() => Promise.resolve({})); + + const createIntermediaryEventFn = jest.fn(); + magic.web3modal.createIntermediaryEvent = jest.fn().mockImplementation(() => createIntermediaryEventFn); + magic.web3modal.connectToWeb3modal(); + const connectedEvent = magic.web3modal.createIntermediaryEvent.mock.calls[0]; + expect(connectedEvent[0]).toBe('wallet_connected'); + expect(setIsConnectedSpy).toBeCalled(); + expect(setEip1193EventListenersSpy).toBeCalled(); +}); + +// skip because it does not like calling the unsubscribe function +test.skip('connectToWeb3modal emits wallet_rejected event on "MODAL_CLOSE" event', () => { + const magic = createMagicSDKWithExtension({}, [new Web3ModalExtension(web3modalParams)]); + magic.web3modal.modal.subscribeEvents = jest.fn( + ( + callback: (provider: { + data: { + event: string; + }; + }) => void, + ) => { + callback({ data: { event: 'MODAL_CLOSE' } }); + }, + ); + const createIntermediaryEventFn = jest.fn(); + magic.web3modal.createIntermediaryEvent = jest.fn().mockImplementation(() => createIntermediaryEventFn); + magic.web3modal.connectToWeb3modal(); + const rejectedEvent = magic.web3modal.createIntermediaryEvent.mock.calls[0]; + expect(rejectedEvent[0]).toBe('wallet_rejected'); +}); diff --git a/packages/@magic-ext/web3modal-ethers/test/spec/constructor.spec.ts b/packages/@magic-ext/web3modal-ethers/test/spec/constructor.spec.ts new file mode 100644 index 000000000..045037bad --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers/test/spec/constructor.spec.ts @@ -0,0 +1,51 @@ +import browserEnv from '@ikscodes/browser-env'; +import { Web3ModalExtension } from '../../src/index'; +import { createMagicSDKWithExtension } from '../../../../@magic-sdk/provider/test/factories'; +import { mockLocalStorage } from '../../../../@magic-sdk/provider/test/mocks'; + +jest.mock('@web3modal/ethers5', () => ({ + Web3Modal: jest.fn(), + defaultConfig: jest.fn(), + createWeb3Modal: jest.fn(() => { + return { + getIsConnected: jest.fn(), + getAddress: jest.fn(() => '0x123'), + getChainId: jest.fn(() => 1), + subscribeProvider: jest.fn(), + }; + }), +})); + +beforeEach(() => { + browserEnv.restore(); + jest.useFakeTimers(); + mockLocalStorage(); +}); + +const web3modalParams = { + configOptions: {}, + modalOptions: { + projectId: '123', + chains: [], + ethersConfig: { metadata: { name: 'test', description: 'test', url: 'test', icons: [] } }, + }, +}; + +test('constructor sets up modal and getIsConnected is false', () => { + const magic = createMagicSDKWithExtension({}, [new Web3ModalExtension(web3modalParams)]); + magic.web3modal.modal.getIsConnected.mockReturnValueOnce(false); + expect(magic.web3modal.modal).toBeDefined(); + expect(magic.web3modal.modal.getIsConnected()).toBeFalsy(); +}); + +// test('constructor sets event listeners when getIsConnected is true', () => { +// const magic = createMagicSDKWithExtension({}, [new Web3ModalExtension(web3modalParams)]); +// magic.web3modal.modal.getIsConnected = jest.fn().mockReturnValue(true); +// const setIsConnectedSpy = jest.spyOn(magic.web3modal, 'setIsConnected').mockImplementation(() => Promise.resolve({})); +// const setEip1193EventListenersSpy = jest +// .spyOn(magic.web3modal, 'setEip1193EventListeners') +// .mockImplementation(() => Promise.resolve({})); +// jest.runAllTimers(); +// expect(setIsConnectedSpy).toBeCalled(); +// expect(setEip1193EventListenersSpy).toBeCalled(); +// }); diff --git a/packages/@magic-ext/web3modal-ethers/test/spec/initialize.spec.ts b/packages/@magic-ext/web3modal-ethers/test/spec/initialize.spec.ts new file mode 100644 index 000000000..25af40577 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers/test/spec/initialize.spec.ts @@ -0,0 +1,37 @@ +import browserEnv from '@ikscodes/browser-env'; +import { Web3ModalExtension } from '../../src/index'; +import { createMagicSDKWithExtension } from '../../../../@magic-sdk/provider/test/factories'; +import { mockLocalStorage } from '../../../../@magic-sdk/provider/test/mocks'; + +jest.mock('@web3modal/ethers5', () => ({ + Web3Modal: jest.fn(), + defaultConfig: jest.fn(), + createWeb3Modal: jest.fn(() => { + return { + subscribeProvider: jest.fn(), + }; + }), +})); + +beforeEach(() => { + browserEnv.restore(); + jest.useFakeTimers(); + mockLocalStorage(); +}); + +const web3modalParams = { + configOptions: {}, + modalOptions: { + projectId: '123', + chains: [], + ethersConfig: { metadata: { name: 'test', description: 'test', url: 'test', icons: [] } }, + }, +}; + +test('initialize updates `enabledWallets`, `isConnected`, and `eventListeners`', () => { + const magic = createMagicSDKWithExtension({}, [new Web3ModalExtension(web3modalParams)]); + magic.web3modal.initialize(); + expect(magic.thirdPartyWallets.enabledWallets.web3modal).toBeTruthy(); + expect(magic.thirdPartyWallets.isConnected).toBeFalsy(); + expect(magic.thirdPartyWallets.eventListeners.length).toEqual(1); +}); diff --git a/packages/@magic-ext/web3modal-ethers/test/spec/setEventListeners.spec.ts b/packages/@magic-ext/web3modal-ethers/test/spec/setEventListeners.spec.ts new file mode 100644 index 000000000..f020da0c6 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers/test/spec/setEventListeners.spec.ts @@ -0,0 +1,47 @@ +import browserEnv from '@ikscodes/browser-env'; +import { Web3ModalExtension } from '../../src/index'; +import { createMagicSDKWithExtension } from '../../../../@magic-sdk/provider/test/factories'; +import { mockLocalStorage } from '../../../../@magic-sdk/provider/test/mocks'; + +jest.mock('@web3modal/ethers5', () => ({ + Web3Modal: jest.fn(), + defaultConfig: jest.fn(), + createWeb3Modal: jest.fn(() => { + return { + getIsConnected: jest.fn(), + getAddress: jest.fn(() => '0x123'), + getChainId: jest.fn(() => 1), + subscribeProvider: jest.fn(), + }; + }), +})); + +beforeEach(() => { + browserEnv.restore(); + mockLocalStorage(); +}); + +const web3modalParams = { + configOptions: {}, + modalOptions: { + projectId: '123', + chains: [], + ethersConfig: { metadata: { name: 'test', description: 'test', url: 'test', icons: [] } }, + }, +}; + +test('setEip1193EventListeners calls subscribeProvider', () => { + const magic = createMagicSDKWithExtension({}, [new Web3ModalExtension(web3modalParams)]); + magic.web3modal.setEip1193EventListeners(); + // once in constructor and once in setEip1193EventListeners + expect(magic.web3modal.modal.subscribeProvider).toBeCalledTimes(2); +}); + +test('setEip1193EventListeners does not set listeners if they were already set', () => { + const magic = createMagicSDKWithExtension({}, [new Web3ModalExtension(web3modalParams)]); + magic.web3modal.eventsListenerAdded = true; + const subscribeProviderSpy = jest.spyOn(magic.web3modal.modal, 'subscribeProvider'); + magic.web3modal.setEip1193EventListeners(); + // only once in constructor + expect(subscribeProviderSpy).toBeCalledTimes(1); +}); diff --git a/packages/@magic-ext/web3modal-ethers/test/spec/setIsConnected.spec.ts b/packages/@magic-ext/web3modal-ethers/test/spec/setIsConnected.spec.ts new file mode 100644 index 000000000..7fe44bcf9 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers/test/spec/setIsConnected.spec.ts @@ -0,0 +1,40 @@ +import browserEnv from '@ikscodes/browser-env'; +import { Web3ModalExtension } from '../../src/index'; +import { createMagicSDKWithExtension } from '../../../../@magic-sdk/provider/test/factories'; +import { mockLocalStorage } from '../../../../@magic-sdk/provider/test/mocks'; + +jest.mock('@web3modal/ethers5', () => ({ + Web3Modal: jest.fn(), + defaultConfig: jest.fn(), + createWeb3Modal: jest.fn(() => { + return { + getIsConnected: jest.fn(), + getAddress: jest.fn(() => '0x123'), + getChainId: jest.fn(() => 1), + subscribeProvider: jest.fn(), + }; + }), +})); + +beforeEach(() => { + browserEnv.restore(); + mockLocalStorage(); +}); + +const web3modalParams = { + configOptions: {}, + modalOptions: { + projectId: '123', + chains: [], + ethersConfig: { metadata: { name: 'test', description: 'test', url: 'test', icons: [] } }, + }, +}; + +test('setIsConnected sets localStorage values', () => { + const magic = createMagicSDKWithExtension({}, [new Web3ModalExtension(web3modalParams)]); + magic.web3modal.setIsConnected(); + expect(localStorage.getItem('magic_3pw_provider')).toEqual('web3modal'); + expect(localStorage.getItem('magic_3pw_address')).toEqual('0x123'); + expect(localStorage.getItem('magic_3pw_chainId')).toEqual('1'); + expect(magic.thirdPartyWallets.isConnected).toBeTruthy(); +}); diff --git a/packages/@magic-ext/web3modal-ethers/test/tsconfig.json b/packages/@magic-ext/web3modal-ethers/test/tsconfig.json new file mode 100644 index 000000000..ef2de78d3 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers/test/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../../../tsconfig.settings.test.json", +} diff --git a/packages/@magic-ext/web3modal-ethers/tsconfig.json b/packages/@magic-ext/web3modal-ethers/tsconfig.json new file mode 100644 index 000000000..c268bce54 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../tsconfig.settings.json", +} + diff --git a/yarn.lock b/yarn.lock index 96efe7877..5d8b67712 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2685,6 +2685,17 @@ __metadata: languageName: unknown linkType: soft +"@magic-ext/web3modal-ethers@workspace:packages/@magic-ext/web3modal-ethers": + version: 0.0.0-use.local + resolution: "@magic-ext/web3modal-ethers@workspace:packages/@magic-ext/web3modal-ethers" + dependencies: + "@magic-sdk/commons": ^24.0.2 + "@magic-sdk/types": 24.0.2-canary.742.9175925480.0 + "@web3modal/ethers": ^5.0.6 + ethers: ^6.13.1 + languageName: unknown + linkType: soft + "@magic-ext/webauthn@workspace:packages/@magic-ext/webauthn": version: 0.0.0-use.local resolution: "@magic-ext/webauthn@workspace:packages/@magic-ext/webauthn" @@ -5620,6 +5631,16 @@ __metadata: languageName: node linkType: hard +"@web3modal/common@npm:5.0.6": + version: 5.0.6 + resolution: "@web3modal/common@npm:5.0.6" + dependencies: + bignumber.js: 9.1.2 + dayjs: 1.11.10 + checksum: 3043e8de9d439a62b37bc323e57f231ec90d4d77674d5d50dc713595672577bfebc5cf617cc929222fc98bbcffb4c4783542285ede3c93df39ef3235b9508fdc + languageName: node + linkType: hard + "@web3modal/core@npm:5.0.3": version: 5.0.3 resolution: "@web3modal/core@npm:5.0.3" @@ -5631,6 +5652,17 @@ __metadata: languageName: node linkType: hard +"@web3modal/core@npm:5.0.6": + version: 5.0.6 + resolution: "@web3modal/core@npm:5.0.6" + dependencies: + "@web3modal/common": 5.0.6 + "@web3modal/wallet": 5.0.6 + valtio: 1.11.2 + checksum: 674d51b1dcbbf75240204aea36f3b8ea3895067ebe881be7b0934d5730f3b73961869ddebb7b0132274adb2e0bb927e46ddaf8bca5c95b9b4e1aede1807c3f4d + languageName: node + linkType: hard + "@web3modal/ethers5@npm:5.0.3": version: 5.0.3 resolution: "@web3modal/ethers5@npm:5.0.3" @@ -5660,6 +5692,35 @@ __metadata: languageName: node linkType: hard +"@web3modal/ethers@npm:^5.0.6": + version: 5.0.6 + resolution: "@web3modal/ethers@npm:5.0.6" + dependencies: + "@coinbase/wallet-sdk": 4.0.3 + "@walletconnect/ethereum-provider": 2.13.0 + "@web3modal/polyfills": 5.0.6 + "@web3modal/scaffold": 5.0.6 + "@web3modal/scaffold-react": 5.0.6 + "@web3modal/scaffold-utils": 5.0.6 + "@web3modal/scaffold-vue": 5.0.6 + "@web3modal/siwe": 5.0.6 + valtio: 1.11.2 + peerDependencies: + ethers: ">=6.0.0" + react: ">=17" + react-dom: ">=17" + vue: ">=3" + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + vue: + optional: true + checksum: f9dd2dfc01e2955735db5b47beabd49d2333f73a2ac87ba6849ca8272b0b7c7294aa43dea31045894703a2532b835e61da3dc77ee273b360363b8dcc7e258b80 + languageName: node + linkType: hard + "@web3modal/polyfills@npm:5.0.3": version: 5.0.3 resolution: "@web3modal/polyfills@npm:5.0.3" @@ -5669,6 +5730,15 @@ __metadata: languageName: node linkType: hard +"@web3modal/polyfills@npm:5.0.6": + version: 5.0.6 + resolution: "@web3modal/polyfills@npm:5.0.6" + dependencies: + buffer: 6.0.3 + checksum: 89878e203bff860eee48ddcc88541a0c4dff1114db61d064ed17644abc2ff5c69f384bc8a1919b24171200f8f701d6b29fea146d29e8d84ba55b97db2e4b4466 + languageName: node + linkType: hard + "@web3modal/scaffold-react@npm:5.0.3": version: 5.0.3 resolution: "@web3modal/scaffold-react@npm:5.0.3" @@ -5686,6 +5756,23 @@ __metadata: languageName: node linkType: hard +"@web3modal/scaffold-react@npm:5.0.6": + version: 5.0.6 + resolution: "@web3modal/scaffold-react@npm:5.0.6" + dependencies: + "@web3modal/scaffold": 5.0.6 + peerDependencies: + react: ">=17" + react-dom: ">=17" + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + checksum: b3aa728347850db939f116be2c555d816df968dc10c3f903c3c6f2df6ad73b114789f6902261da9a9aab4c363a55a76107b9bdb4f0d31cce7d53c0a1d30f7bed + languageName: node + linkType: hard + "@web3modal/scaffold-ui@npm:5.0.3": version: 5.0.3 resolution: "@web3modal/scaffold-ui@npm:5.0.3" @@ -5701,6 +5788,21 @@ __metadata: languageName: node linkType: hard +"@web3modal/scaffold-ui@npm:5.0.6": + version: 5.0.6 + resolution: "@web3modal/scaffold-ui@npm:5.0.6" + dependencies: + "@web3modal/common": 5.0.6 + "@web3modal/core": 5.0.6 + "@web3modal/scaffold-utils": 5.0.6 + "@web3modal/siwe": 5.0.6 + "@web3modal/ui": 5.0.6 + "@web3modal/wallet": 5.0.6 + lit: 3.1.0 + checksum: 821270baecbeeaf30e3cf37a679bd271710f4bdf8d0529bcf2bcd7659a310fdc571f4b0e89583ce47ca6b43acc6ce88c489cd6e1376fec555b9a1e8366aed305 + languageName: node + linkType: hard + "@web3modal/scaffold-utils@npm:5.0.3": version: 5.0.3 resolution: "@web3modal/scaffold-utils@npm:5.0.3" @@ -5712,6 +5814,17 @@ __metadata: languageName: node linkType: hard +"@web3modal/scaffold-utils@npm:5.0.6": + version: 5.0.6 + resolution: "@web3modal/scaffold-utils@npm:5.0.6" + dependencies: + "@web3modal/core": 5.0.6 + "@web3modal/polyfills": 5.0.6 + valtio: 1.11.2 + checksum: 226f7545b0ab0066a50b52959c63493b9df250026d49dbf418fa16ba5c4491580a6fa901de80bb4d023671bf0cce36ba41ae0d720a71d2681f1496a5302093c9 + languageName: node + linkType: hard + "@web3modal/scaffold-vue@npm:5.0.3": version: 5.0.3 resolution: "@web3modal/scaffold-vue@npm:5.0.3" @@ -5726,6 +5839,20 @@ __metadata: languageName: node linkType: hard +"@web3modal/scaffold-vue@npm:5.0.6": + version: 5.0.6 + resolution: "@web3modal/scaffold-vue@npm:5.0.6" + dependencies: + "@web3modal/scaffold": 5.0.6 + peerDependencies: + vue: ">=3" + peerDependenciesMeta: + vue: + optional: true + checksum: e5bf43bf0e4ce54c118631a5b714a784d3f0f6cb9ad02174aa8a9ea3fd3a5b3dc25b8376c3b83318a9819ccfeccc7cb51a5392a9d3a03d34f98f8fbcdf68442f + languageName: node + linkType: hard + "@web3modal/scaffold@npm:5.0.3": version: 5.0.3 resolution: "@web3modal/scaffold@npm:5.0.3" @@ -5742,6 +5869,22 @@ __metadata: languageName: node linkType: hard +"@web3modal/scaffold@npm:5.0.6": + version: 5.0.6 + resolution: "@web3modal/scaffold@npm:5.0.6" + dependencies: + "@web3modal/common": 5.0.6 + "@web3modal/core": 5.0.6 + "@web3modal/scaffold-ui": 5.0.6 + "@web3modal/scaffold-utils": 5.0.6 + "@web3modal/siwe": 5.0.6 + "@web3modal/ui": 5.0.6 + "@web3modal/wallet": 5.0.6 + lit: 3.1.0 + checksum: b4f4dbfbd2f47a7088180e588dd508698516c9ba99e5bc7bb04714092edc5273a408c82e0392b644e9e9605281220a21f557703a5fc337f3a3a7dcabc7f3cc2f + languageName: node + linkType: hard + "@web3modal/siwe@npm:5.0.3": version: 5.0.3 resolution: "@web3modal/siwe@npm:5.0.3" @@ -5755,6 +5898,19 @@ __metadata: languageName: node linkType: hard +"@web3modal/siwe@npm:5.0.6": + version: 5.0.6 + resolution: "@web3modal/siwe@npm:5.0.6" + dependencies: + "@walletconnect/utils": 2.12.0 + "@web3modal/core": 5.0.6 + "@web3modal/scaffold-utils": 5.0.6 + lit: 3.1.0 + valtio: 1.11.2 + checksum: 3a7e97c2e79f98a7ebea02038d88933a27c99b5465fd20406248608eeb904e12c38c17646045d7104a681f1b736045c8c3c0a5d30623e03d7b3030f58921e024 + languageName: node + linkType: hard + "@web3modal/ui@npm:5.0.3": version: 5.0.3 resolution: "@web3modal/ui@npm:5.0.3" @@ -5765,6 +5921,16 @@ __metadata: languageName: node linkType: hard +"@web3modal/ui@npm:5.0.6": + version: 5.0.6 + resolution: "@web3modal/ui@npm:5.0.6" + dependencies: + lit: 3.1.0 + qrcode: 1.5.3 + checksum: bf670bd43869f0c4e10408bb13116792a0ce012e036d761488e8680f59835ca79c2fbe7ddb8ceb3734b842bbe99c6923a81b8ab6fe09caaeb0ce0c9eabbece28 + languageName: node + linkType: hard + "@web3modal/wallet@npm:5.0.3": version: 5.0.3 resolution: "@web3modal/wallet@npm:5.0.3" @@ -5776,6 +5942,17 @@ __metadata: languageName: node linkType: hard +"@web3modal/wallet@npm:5.0.6": + version: 5.0.6 + resolution: "@web3modal/wallet@npm:5.0.6" + dependencies: + "@walletconnect/logger": 2.1.2 + "@web3modal/polyfills": 5.0.6 + zod: 3.22.4 + checksum: 9880ab69164f78cd2465d97b15ae70b11a4f14a7216a6d48633dc803310db2514a2a5bc577e03393380210b050ba4e486716ed67e0dca00993fa9c9e576bdf1e + languageName: node + linkType: hard + "@yarnpkg/lockfile@npm:^1.1.0": version: 1.1.0 resolution: "@yarnpkg/lockfile@npm:1.1.0" @@ -10536,7 +10713,7 @@ __metadata: languageName: node linkType: hard -"ethers@npm:^6.13.0": +"ethers@npm:^6.13.0, ethers@npm:^6.13.1": version: 6.13.1 resolution: "ethers@npm:6.13.1" dependencies: