diff --git a/.github/workflows/deploy-cdn.yml b/.github/workflows/deploy-cdn.yml new file mode 100644 index 000000000..530461eba --- /dev/null +++ b/.github/workflows/deploy-cdn.yml @@ -0,0 +1,43 @@ +name: Deploy to CDN + +on: + workflow_dispatch: + inputs: + tag: + description: 'Tag to deploy' + required: true + default: '' + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + ref: ${{ github.event.inputs.tag }} + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + + - name: Install Yarn 3 + run: yarn set version 3.5.1 + + - name: Install Dependencies + run: yarn install --immutable + + - name: Run deploy script + env: + tag: ${{ github.event.inputs.tag }} + run: bash ./scripts/deploy-cdn.sh + + - name: Deploy dapps + uses: peaceiris/actions-gh-pages@068dc23d9710f1ba62e86896f84735d869951305 + with: + personal_token: ${{ secrets.DEPLOY_TOKEN }} + # force_orphan: true # removing for now as it is incompatible with keep_files + keep_files: true # Important to keep the rest of the files deployed previously + publish_dir: ./deployments diff --git a/.github/workflows/deploy-static.yml b/.github/workflows/deploy-static.yml new file mode 100644 index 000000000..c988422e9 --- /dev/null +++ b/.github/workflows/deploy-static.yml @@ -0,0 +1,43 @@ +name: Deploy to CDN + +on: + workflow_dispatch: + inputs: + tag: + description: 'Tag to deploy' + required: true + default: '' + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + ref: ${{ github.event.inputs.tag }} + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + + - name: Install Yarn 3 + run: yarn set version 3.5.1 + + - name: Install Dependencies + run: yarn install --immutable + + - name: Run deploy script + env: + tag: ${{ github.event.inputs.tag }} + run: bash ./scripts/deploy-static.sh + + - name: Deploy dapps + uses: peaceiris/actions-gh-pages@068dc23d9710f1ba62e86896f84735d869951305 + with: + personal_token: ${{ secrets.DEPLOY_TOKEN }} + # force_orphan: true # removing for now as it is incompatible with keep_files + keep_files: true # Important to keep the rest of the files deployed previously + publish_dir: ./deployments diff --git a/packages/sdk/src/services/Ethereum.ts b/packages/sdk/src/services/Ethereum.ts index e520aa2e0..37ac2e35e 100644 --- a/packages/sdk/src/services/Ethereum.ts +++ b/packages/sdk/src/services/Ethereum.ts @@ -35,19 +35,37 @@ export class Ethereum { autoRequestAccounts: false, }); - const proxiedProvieer = new Proxy(provider, { - // some common libraries, e.g. web3@1.x, can confict with our API. + const proxiedProvider = new Proxy(provider, { + // some common libraries, e.g. web3@1.x, can conflict with our API. deleteProperty: () => true, }); - this.provider = proxiedProvieer; + this.provider = proxiedProvider; this.sdkInstance = sdkInstance; + + // Add try-catch block around window modifications if (shouldSetOnWindow && typeof window !== 'undefined') { - setGlobalProvider(provider); + try { + setGlobalProvider(provider); + } catch (error) { + logger( + '[Ethereum] Unable to set global provider - window.ethereum may be read-only', + error, + ); + // Continue execution without throwing + } } if (shouldShimWeb3 && typeof window !== 'undefined') { - shimWeb3(this.provider); + try { + shimWeb3(this.provider); + } catch (error) { + logger( + '[Ethereum] Unable to shim web3 - window.web3 may be read-only', + error, + ); + // Continue execution without throwing + } } // Propagate display_uri events to the SDK diff --git a/packages/sdk/src/utils/get-browser-extension.test.ts b/packages/sdk/src/utils/get-browser-extension.test.ts index 5ced07f1b..baa45b0f7 100644 --- a/packages/sdk/src/utils/get-browser-extension.test.ts +++ b/packages/sdk/src/utils/get-browser-extension.test.ts @@ -1,6 +1,6 @@ import { MetaMaskSDK } from '../sdk'; -import { getBrowserExtension } from './get-browser-extension'; import { eip6963RequestProvider } from './eip6963RequestProvider'; +import { getBrowserExtension } from './get-browser-extension'; jest.mock('./eip6963RequestProvider'); @@ -9,127 +9,55 @@ describe('getBrowserExtension', () => { beforeEach(() => { jest.clearAllMocks(); - + global.window = {} as any; sdkInstance = { - options: { - dappMetadata: {}, - }, - platformManager: { - getPlatformType: jest.fn(), - }, + options: { dappMetadata: {} }, + platformManager: { getPlatformType: jest.fn() }, } as unknown as MetaMaskSDK; }); - it('should throw an error if window is undefined', async () => { + afterEach(() => { global.window = undefined as any; + }); + it('should throw if window is undefined', async () => { + global.window = undefined as any; await expect( getBrowserExtension({ mustBeMetaMask: true, sdkInstance }), ).rejects.toThrow('window not available'); }); - it('should return baseProvider if eip6963RequestProvider resolves successfully', async () => { + it('should return provider from EIP-6963 if available', async () => { const mockProvider = { isMetaMask: true }; - global.window = { ethereum: {} } as any; (eip6963RequestProvider as jest.Mock).mockResolvedValue(mockProvider); - const res = await getBrowserExtension({ - mustBeMetaMask: true, - sdkInstance, - }); - - expect(res).toStrictEqual(mockProvider); - }); - - it('should throw an error if eip6963RequestProvider rejects and ethereum is not found in window object', async () => { - (eip6963RequestProvider as jest.Mock).mockRejectedValue( - new Error('Provider request failed'), - ); - global.window = {} as any; - - await expect( - getBrowserExtension({ mustBeMetaMask: true, sdkInstance }), - ).rejects.toThrow('Ethereum not found in window object'); - }); - - it('should throw an error if no suitable provider is found', async () => { - (eip6963RequestProvider as jest.Mock).mockRejectedValue( - new Error('Provider request failed'), - ); - global.window = { ethereum: { providers: [] } } as any; - - await expect( - getBrowserExtension({ mustBeMetaMask: true, sdkInstance }), - ).rejects.toThrow('No suitable provider found'); - }); - - it('should return MetaMask provider if mustBeMetaMask is true', async () => { - const mockProvider = { isMetaMask: true }; - (eip6963RequestProvider as jest.Mock).mockRejectedValue( - new Error('Provider request failed'), - ); - global.window = { ethereum: { providers: [mockProvider] } } as any; - - const res = await getBrowserExtension({ + const result = await getBrowserExtension({ mustBeMetaMask: true, sdkInstance, }); - - expect(res).toStrictEqual(mockProvider); + expect(result).toStrictEqual(expect.objectContaining({ isMetaMask: true })); }); - it('should return the first provider if mustBeMetaMask is false', async () => { + it('should fallback to window.ethereum only if not requiring MetaMask', async () => { const mockProvider = { isMetaMask: false }; - (eip6963RequestProvider as jest.Mock).mockRejectedValue( - new Error('Provider request failed'), - ); - global.window = { ethereum: { providers: [mockProvider] } } as any; + (eip6963RequestProvider as jest.Mock).mockRejectedValue(new Error()); + global.window = { ethereum: mockProvider } as any; - const res = await getBrowserExtension({ + const result = await getBrowserExtension({ mustBeMetaMask: false, sdkInstance, }); - - expect(res).toStrictEqual(mockProvider); - }); - - it('should throw an error if mustBeMetaMask is true but MetaMask provider not found', async () => { - (eip6963RequestProvider as jest.Mock).mockRejectedValue( - new Error('Provider request failed'), + expect(result).toStrictEqual( + expect.objectContaining({ isMetaMask: false }), ); - global.window = { ethereum: { isMetaMask: false } } as any; - - await expect( - getBrowserExtension({ mustBeMetaMask: true, sdkInstance }), - ).rejects.toThrow('MetaMask provider not found in Ethereum'); }); - it('should throw an error if mustBeMetaMask is true but uniswap wallet installed instead of Metamask', async () => { - (eip6963RequestProvider as jest.Mock).mockRejectedValue( - new Error('Provider request failed'), - ); - - global.window = { - ethereum: { isMetaMask: true, isUniswapWallet: true }, - } as any; + it('should throw if no provider found', async () => { + (eip6963RequestProvider as jest.Mock).mockRejectedValue(new Error()); + global.window = {} as any; await expect( - getBrowserExtension({ mustBeMetaMask: true, sdkInstance }), - ).rejects.toThrow('MetaMask provider not found in Ethereum'); - }); - - it('should return ethereum object if mustBeMetaMask is false and ethereum object exists', async () => { - const ethereumObj = { isMetaMask: true }; - (eip6963RequestProvider as jest.Mock).mockRejectedValue( - new Error('Provider request failed'), - ); - global.window = { ethereum: ethereumObj } as any; - - const res = await getBrowserExtension({ - mustBeMetaMask: false, - sdkInstance, - }); - - expect(res).toStrictEqual(ethereumObj); + getBrowserExtension({ mustBeMetaMask: false, sdkInstance }), + ).rejects.toThrow('Provider not found'); }); }); diff --git a/packages/sdk/src/utils/get-browser-extension.ts b/packages/sdk/src/utils/get-browser-extension.ts index 8e58606cd..ad4f03f26 100644 --- a/packages/sdk/src/utils/get-browser-extension.ts +++ b/packages/sdk/src/utils/get-browser-extension.ts @@ -11,85 +11,22 @@ export async function getBrowserExtension({ sdkInstance: MetaMaskSDK; }): Promise { if (typeof window === 'undefined') { - throw new Error(`window not available`); + throw new Error('window not available'); } - let extensionProvider: MetaMaskInpageProvider; - try { - extensionProvider = await eip6963RequestProvider(); + // Try EIP-6963 first + const extensionProvider = await eip6963RequestProvider(); return wrapExtensionProvider({ provider: extensionProvider, sdkInstance }); } catch (e) { - const { ethereum } = window; - - if (!ethereum) { - throw new Error('Ethereum not found in window object'); - } - - // The `providers` field is populated when CoinBase Wallet extension is also installed - // The expected object is an array of providers, the MetaMask provider is inside - // See https://docs.cloud.coinbase.com/wallet-sdk/docs/injected-provider-guidance for - if ('providers' in ethereum) { - if (Array.isArray(ethereum.providers)) { - const provider = mustBeMetaMask - ? ethereum.providers.find((p: any) => - isRealMetaMaskExtensionInstalled(p), - ) - : ethereum.providers[0]; - - if (!provider) { - throw new Error('No suitable provider found'); - } - - return wrapExtensionProvider({ provider, sdkInstance }); - } - } else if (mustBeMetaMask && !isRealMetaMaskExtensionInstalled(ethereum)) { - throw new Error('MetaMask provider not found in Ethereum'); + // Legacy fallback only for non-MetaMask cases + if (!mustBeMetaMask && window.ethereum) { + return wrapExtensionProvider({ + provider: window.ethereum, + sdkInstance, + }); } - return wrapExtensionProvider({ - provider: ethereum, - sdkInstance, - }); + throw new Error('Provider not found'); } } - -function isRealMetaMaskExtensionInstalled(eth: any) { - if (!eth.isMetaMask) { - return false; - } - - // Brave tries to make itself look like MetaMask - // Could also try RPC `web3_clientVersion` if following is unreliable - if (eth.isBraveWallet && !eth._events && !eth._state) { - return false; - } - - // Other wallets that try to look like MetaMask - const flags: string[] = [ - 'isApexWallet', - 'isAvalanche', - 'isBitKeep', - 'isBlockWallet', - 'isKuCoinWallet', - 'isMathWallet', - 'isOkxWallet', - 'isOKExWallet', - 'isOneInchIOSWallet', - 'isOneInchAndroidWallet', - 'isOpera', - 'isPortal', - 'isRabby', - 'isTokenPocket', - 'isTokenary', - 'isUniswapWallet', - 'isZerion', - ]; - for (const flag of flags) { - if (eth[flag]) { - return false; - } - } - - return true; -} diff --git a/scripts/deploy-cdn.sh b/scripts/deploy-cdn.sh new file mode 100644 index 000000000..ca242af75 --- /dev/null +++ b/scripts/deploy-cdn.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# Stop on first error +set -e + +# Make sure to start from base workspace folder +reldir="$( dirname -- "$0"; )"; +cd "$reldir/.."; + +build_sdk() { + echo "\n---------- Building SDK -------------\n" + yarn build + echo "\n---------- Done building SDK -------------\n" +} + +copy_to_deployment_dir() { + local deployment_dir=$1 + echo "\n---------- Copying to deployment directory -------------\n" + cp -r packages/sdk/dist/browser/iife/metamask-sdk.js "$deployment_dir" + cp -r packages/sdk/dist/browser/iife/metamask-sdk.js.map "$deployment_dir" + echo "\n---------- Done copying to deployment directory -------------\n" +} + +# ------ Start +deployment_folder="cdn" +gh_tag=$tag +gh_tag_version=$(echo "$gh_tag" | sed -E 's/@metamask\/sdk@([0-9]+\.[0-9]+\.[0-9]+).*/\1/') + +# Sanitize tag version +deployment_dir="deployments/$deployment_folder/$gh_tag_version" + +echo "Deployment folder: $deployment_folder" +echo "Deployment directory: $deployment_dir" + +mkdir -p "$deployment_dir" + +# Build SDK from root +build_sdk + +# Copy to deployment directory +copy_to_deployment_dir "$deployment_dir" \ No newline at end of file diff --git a/scripts/deploy-static.sh b/scripts/deploy-static.sh new file mode 100644 index 000000000..eb399e80a --- /dev/null +++ b/scripts/deploy-static.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# Stop on first error +set -e + +# Make sure to start from base workspace folder +reldir="$( dirname -- "$0"; )"; +cd "$reldir/.."; + +build_sdk() { + echo "\n---------- Building SDK -------------\n" + yarn build + echo "\n---------- Done building SDK -------------\n" +} + +copy_to_deployment_dir() { + local deployment_dir=$1 + echo "\n---------- Copying to deployment directory -------------\n" + cp -r packages/sdk/dist/browser/iife/metamask-sdk.js "$deployment_dir" + cp -r packages/sdk/dist/browser/iife/metamask-sdk.js.map "$deployment_dir" + echo "\n---------- Done copying to deployment directory -------------\n" +} + +# ------ Start +deployment_folder="static" +gh_tag=$tag +gh_tag_version=$(echo "$gh_tag" | sed -E 's/@metamask\/sdk@([0-9]+\.[0-9]+\.[0-9]+).*/\1/') + +# Sanitize tag version +deployment_dir="deployments/$deployment_folder/$gh_tag_version" + +echo "Deployment folder: $deployment_folder" +echo "Deployment directory: $deployment_dir" + +mkdir -p "$deployment_dir" + +# Build SDK from root +build_sdk + +# Copy to deployment directory +copy_to_deployment_dir "$deployment_dir" \ No newline at end of file