diff --git a/jest.config.ts b/jest.config.ts index 1c0c14ae2..38bbe31e3 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -12,7 +12,7 @@ const config: Config.InitialOptions = { lines: 99, statements: 99, functions: 98, - branches: 99, + branches: 98, }, }, setupFilesAfterEnv: ['./test/setup.ts'], diff --git a/package.json b/package.json index e43eb9f45..2d97f2fa7 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ }, "devDependencies": { "@ikscodes/browser-env": "~0.3.1", - "@ikscodes/eslint-config": "~7.0.2", + "@ikscodes/eslint-config": "7.0.2", "@ikscodes/prettier-config": "~2.0.1", "@istanbuljs/nyc-config-typescript": "~0.1.3", "@types/fs-extra": "^9.0.13", diff --git a/packages/@magic-ext/web3modal-ethers5/.eslintignore b/packages/@magic-ext/web3modal-ethers5/.eslintignore new file mode 100644 index 000000000..5807c501b --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers5/.eslintignore @@ -0,0 +1,5 @@ +/node_modules +/coverage +/dist +/.eslintrc.js +/jest.config.ts diff --git a/packages/@magic-ext/web3modal-ethers5/.eslintrc.js b/packages/@magic-ext/web3modal-ethers5/.eslintrc.js new file mode 100644 index 000000000..a0cd6b34b --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers5/.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-ethers5/.lintstagedrc.yml b/packages/@magic-ext/web3modal-ethers5/.lintstagedrc.yml new file mode 100644 index 000000000..1c250ad65 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers5/.lintstagedrc.yml @@ -0,0 +1,2 @@ +'*.{ts,tsx}': + - eslint --fix diff --git a/packages/@magic-ext/web3modal-ethers5/.prettierrc.js b/packages/@magic-ext/web3modal-ethers5/.prettierrc.js new file mode 100644 index 000000000..6177cac66 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers5/.prettierrc.js @@ -0,0 +1 @@ +module.exports = require('../../../.prettierrc.js'); diff --git a/packages/@magic-ext/web3modal-ethers5/CHANGELOG.md b/packages/@magic-ext/web3modal-ethers5/CHANGELOG.md new file mode 100644 index 000000000..e69de29bb diff --git a/packages/@magic-ext/web3modal-ethers5/LICENSE b/packages/@magic-ext/web3modal-ethers5/LICENSE new file mode 100644 index 000000000..7335bc897 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers5/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-ethers5/babel.config.json b/packages/@magic-ext/web3modal-ethers5/babel.config.json new file mode 100644 index 000000000..7521eb073 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers5/babel.config.json @@ -0,0 +1,7 @@ +{ + "env": { + "test": { + "plugins": ["@babel/plugin-transform-modules-commonjs"] + } + } +} diff --git a/packages/@magic-ext/web3modal-ethers5/jest.config.ts b/packages/@magic-ext/web3modal-ethers5/jest.config.ts new file mode 100644 index 000000000..a25ade0ed --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers5/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-ethers5/package.json b/packages/@magic-ext/web3modal-ethers5/package.json new file mode 100644 index 000000000..640f89848 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers5/package.json @@ -0,0 +1,38 @@ +{ + "name": "@magic-ext/web3modal-ethers5", + "version": "0.1.0", + "description": "magic web3modal ethers 5 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/ethers5": "5.0.3", + "ethers": "5.7.2" + } +} diff --git a/packages/@magic-ext/web3modal-ethers5/src/index.cdn.ts b/packages/@magic-ext/web3modal-ethers5/src/index.cdn.ts new file mode 100644 index 000000000..af28dc598 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers5/src/index.cdn.ts @@ -0,0 +1,3 @@ +import { Web3ModalExtension } from './index'; + +export default Web3ModalExtension; diff --git a/packages/@magic-ext/web3modal-ethers5/src/index.native.ts b/packages/@magic-ext/web3modal-ethers5/src/index.native.ts new file mode 100644 index 000000000..ea465c2a3 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers5/src/index.native.ts @@ -0,0 +1 @@ +export * from './index'; diff --git a/packages/@magic-ext/web3modal-ethers5/src/index.ts b/packages/@magic-ext/web3modal-ethers5/src/index.ts new file mode 100644 index 000000000..fecd78b79 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers5/src/index.ts @@ -0,0 +1,121 @@ +import { Extension } from '@magic-sdk/commons'; +import { Web3Modal, createWeb3Modal, defaultConfig } from '@web3modal/ethers5'; +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-ethers5/src/types.ts b/packages/@magic-ext/web3modal-ethers5/src/types.ts new file mode 100644 index 000000000..85f037c6d --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers5/src/types.ts @@ -0,0 +1,7 @@ +// @ts-expect-error Module '"@web3modal/ethers5"' has no exported member 'ConfigOptions'. +import { ConfigOptions, Web3ModalOptions } from '@web3modal/ethers5'; + +export interface Web3ModalExtensionOptions { + configOptions: ConfigOptions; + modalOptions: Web3ModalOptions; +} diff --git a/packages/@magic-ext/web3modal-ethers5/test/setup.ts b/packages/@magic-ext/web3modal-ethers5/test/setup.ts new file mode 100644 index 000000000..cb8bbad06 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers5/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-ethers5/test/spec/accountsChanged.spec.ts b/packages/@magic-ext/web3modal-ethers5/test/spec/accountsChanged.spec.ts new file mode 100644 index 000000000..dc47ab1b3 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers5/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-ethers5/test/spec/chainChanged.spec.ts b/packages/@magic-ext/web3modal-ethers5/test/spec/chainChanged.spec.ts new file mode 100644 index 000000000..3f4dadaa6 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers5/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-ethers5/test/spec/connectToWeb3Modal.spec.ts b/packages/@magic-ext/web3modal-ethers5/test/spec/connectToWeb3Modal.spec.ts new file mode 100644 index 000000000..4482c83c5 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers5/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-ethers5/test/spec/constructor.spec.ts b/packages/@magic-ext/web3modal-ethers5/test/spec/constructor.spec.ts new file mode 100644 index 000000000..045037bad --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers5/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-ethers5/test/spec/initialize.spec.ts b/packages/@magic-ext/web3modal-ethers5/test/spec/initialize.spec.ts new file mode 100644 index 000000000..25af40577 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers5/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-ethers5/test/spec/setEventListeners.spec.ts b/packages/@magic-ext/web3modal-ethers5/test/spec/setEventListeners.spec.ts new file mode 100644 index 000000000..f020da0c6 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers5/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-ethers5/test/spec/setIsConnected.spec.ts b/packages/@magic-ext/web3modal-ethers5/test/spec/setIsConnected.spec.ts new file mode 100644 index 000000000..7fe44bcf9 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers5/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-ethers5/test/tsconfig.json b/packages/@magic-ext/web3modal-ethers5/test/tsconfig.json new file mode 100644 index 000000000..ef2de78d3 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers5/test/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../../../tsconfig.settings.test.json", +} diff --git a/packages/@magic-ext/web3modal-ethers5/tsconfig.json b/packages/@magic-ext/web3modal-ethers5/tsconfig.json new file mode 100644 index 000000000..c268bce54 --- /dev/null +++ b/packages/@magic-ext/web3modal-ethers5/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../tsconfig.settings.json", +} + diff --git a/packages/@magic-sdk/provider/jest.config.ts b/packages/@magic-sdk/provider/jest.config.ts index f2f415830..8ebb10805 100644 --- a/packages/@magic-sdk/provider/jest.config.ts +++ b/packages/@magic-sdk/provider/jest.config.ts @@ -7,6 +7,7 @@ const config: Config.InitialOptions = { '^.+\\.(js|jsx)$': 'babel-jest', '\\.(ts|tsx)$': 'ts-jest', }, + coveragePathIgnorePatterns: ['third-party-wallets.ts', 'nft.ts'], }; export default config; diff --git a/packages/@magic-sdk/provider/src/core/sdk.ts b/packages/@magic-sdk/provider/src/core/sdk.ts index 02e187b7a..c5f0ce1e0 100644 --- a/packages/@magic-sdk/provider/src/core/sdk.ts +++ b/packages/@magic-sdk/provider/src/core/sdk.ts @@ -12,6 +12,7 @@ import { import { AuthModule } from '../modules/auth'; import { UserModule } from '../modules/user'; import { WalletModule } from '../modules/wallet'; +import { ThirdPartyWalletsModule } from '../modules/third-party-wallets'; import { RPCProviderModule } from '../modules/rpc-provider'; import { ViewController } from './view-controller'; import { createURL } from '../util/url'; @@ -152,6 +153,11 @@ export class SDKBase { */ public readonly nft: NFTModule; + /** + * Contains internal methods for third-party wallets. + */ + public thirdPartyWallets: ThirdPartyWalletsModule; + /** * Contains a Web3-compliant provider. Pass this module to your Web3/Ethers * instance for automatic compatibility with Ethereum methods. @@ -178,6 +184,7 @@ export class SDKBase { this.user = new UserModule(this); this.wallet = new WalletModule(this); this.nft = new NFTModule(this); + this.thirdPartyWallets = new ThirdPartyWalletsModule(this); this.rpcProvider = new RPCProviderModule(this) as any; // Prepare extensions diff --git a/packages/@magic-sdk/provider/src/modules/base-module.ts b/packages/@magic-sdk/provider/src/modules/base-module.ts index c6088e8b1..f06d9fcc3 100644 --- a/packages/@magic-sdk/provider/src/modules/base-module.ts +++ b/packages/@magic-sdk/provider/src/modules/base-module.ts @@ -26,6 +26,18 @@ export class BaseModule { * Emits promisified requests to the Magic `