diff --git a/.github/workflows/versioning.yml b/.github/workflows/versioning.yml index b85546e75a..1a0ed710fb 100644 --- a/.github/workflows/versioning.yml +++ b/.github/workflows/versioning.yml @@ -45,8 +45,8 @@ jobs: - name: Install NPM dependencies run: yarn install --immutable - - - name: Update version.ts file + + - name: Update sdk-info.ts file run: yarn prebuild - name: Commit and branch updated version diff --git a/packages/wallet-sdk/package.json b/packages/wallet-sdk/package.json index 736245164a..f4c3a02a90 100644 --- a/packages/wallet-sdk/package.json +++ b/packages/wallet-sdk/package.json @@ -21,7 +21,7 @@ "pretest": "node compile-assets.js", "test": "jest", "test:coverage": "yarn test:unit && open coverage/lcov-report/index.html", - "prebuild": "rm -rf ./dist && node -p \"'export const LIB_VERSION = \\'' + require('./package.json').version + '\\';'\" > src/version.ts", + "prebuild": "rm -rf ./dist && node -p \"'export const VERSION = \\'' + require('./package.json').version + '\\';\\nexport const NAME = \\'' + require('./package.json').name + '\\';'\" > src/sdk-info.ts", "build": "node compile-assets.js && tsc -p ./tsconfig.build.json && tsc-alias && cp -a src/vendor-js dist", "dev": "yarn build && tsc --watch & nodemon --watch dist --delay 1 --exec tsc-alias", "typecheck": "tsc --noEmit", diff --git a/packages/wallet-sdk/src/CoinbaseWalletSDK.test.ts b/packages/wallet-sdk/src/CoinbaseWalletSDK.test.ts index ade93c5caa..70bb0bba15 100644 --- a/packages/wallet-sdk/src/CoinbaseWalletSDK.test.ts +++ b/packages/wallet-sdk/src/CoinbaseWalletSDK.test.ts @@ -7,7 +7,7 @@ import { getCoinbaseInjectedProvider } from ':util/provider'; jest.mock(':core/type/util'); jest.mock(':util/provider'); jest.mock('./CoinbaseWalletProvider'); -jest.mock('./util/crossOriginOpenerPolicy'); +jest.mock('./util/checkCrossOriginOpenerPolicy'); describe('CoinbaseWalletSDK', () => { test('@makeWeb3Provider - return Coinbase Injected Provider', () => { diff --git a/packages/wallet-sdk/src/CoinbaseWalletSDK.ts b/packages/wallet-sdk/src/CoinbaseWalletSDK.ts index b7fcb03046..543a1f8636 100644 --- a/packages/wallet-sdk/src/CoinbaseWalletSDK.ts +++ b/packages/wallet-sdk/src/CoinbaseWalletSDK.ts @@ -3,10 +3,10 @@ import { LogoType, walletLogo } from './assets/wallet-logo'; import { CoinbaseWalletProvider } from './CoinbaseWalletProvider'; import { AppMetadata, Preference, ProviderInterface } from './core/provider/interface'; -import { LIB_VERSION } from './version'; +import { VERSION } from './sdk-info'; import { ScopedLocalStorage } from ':core/storage/ScopedLocalStorage'; import { getFavicon } from ':core/type/util'; -import { checkCrossOriginOpenerPolicy } from ':util/crossOriginOpenerPolicy'; +import { checkCrossOriginOpenerPolicy } from ':util/checkCrossOriginOpenerPolicy'; import { getCoinbaseInjectedProvider } from ':util/provider'; import { validatePreferences } from ':util/validatePreferences'; @@ -29,7 +29,7 @@ export class CoinbaseWalletSDK { appChainIds: metadata.appChainIds || [], }; this.storeLatestVersion(); - this.checkCrossOriginOpenerPolicy(); + void checkCrossOriginOpenerPolicy(); } public makeWeb3Provider(preference: Preference = { options: 'all' }): ProviderInterface { @@ -50,10 +50,6 @@ export class CoinbaseWalletSDK { private storeLatestVersion() { const versionStorage = new ScopedLocalStorage('CBWSDK'); - versionStorage.setItem('VERSION', LIB_VERSION); - } - - private checkCrossOriginOpenerPolicy() { - void checkCrossOriginOpenerPolicy(); + versionStorage.setItem('VERSION', VERSION); } } diff --git a/packages/wallet-sdk/src/core/communicator/Communicator.test.ts b/packages/wallet-sdk/src/core/communicator/Communicator.test.ts index 3616a5e570..5fd91a52f2 100644 --- a/packages/wallet-sdk/src/core/communicator/Communicator.test.ts +++ b/packages/wallet-sdk/src/core/communicator/Communicator.test.ts @@ -1,6 +1,6 @@ import { AppMetadata, Preference } from 'src/index'; -import { LIB_VERSION } from '../../version'; +import { VERSION } from '../../sdk-info'; import { Message, MessageID } from '../message'; import { Communicator } from './Communicator'; import { CB_KEYS_URL } from ':core/constants'; @@ -113,7 +113,7 @@ describe('Communicator', () => { 1, { data: { - version: LIB_VERSION, + version: VERSION, metadata: appMetadata, preference, }, @@ -140,7 +140,7 @@ describe('Communicator', () => { 1, { data: { - version: LIB_VERSION, + version: VERSION, metadata: appMetadata, preference, }, @@ -162,7 +162,7 @@ describe('Communicator', () => { 1, { data: { - version: LIB_VERSION, + version: VERSION, metadata: appMetadata, preference, }, diff --git a/packages/wallet-sdk/src/core/communicator/Communicator.ts b/packages/wallet-sdk/src/core/communicator/Communicator.ts index 88908297f1..69beb52d78 100644 --- a/packages/wallet-sdk/src/core/communicator/Communicator.ts +++ b/packages/wallet-sdk/src/core/communicator/Communicator.ts @@ -1,4 +1,4 @@ -import { LIB_VERSION } from '../../version'; +import { VERSION } from '../../sdk-info'; import { ConfigMessage, Message, MessageID } from '../message'; import { CB_KEYS_URL } from ':core/constants'; import { standardErrors } from ':core/error'; @@ -109,7 +109,7 @@ export class Communicator { this.postMessage({ requestId: message.id, data: { - version: LIB_VERSION, + version: VERSION, metadata: this.metadata, preference: this.preference, }, diff --git a/packages/wallet-sdk/src/core/error/serialize.ts b/packages/wallet-sdk/src/core/error/serialize.ts index 2ade9e1551..1e12b82718 100644 --- a/packages/wallet-sdk/src/core/error/serialize.ts +++ b/packages/wallet-sdk/src/core/error/serialize.ts @@ -1,6 +1,6 @@ // TODO: error should not depend on walletlink. revisit this. +import { VERSION } from '../../sdk-info'; import { isErrorResponse, Web3Response } from '../../sign/walletlink/relay/type/Web3Response'; -import { LIB_VERSION } from '../../version'; import { standardErrorCodes } from './constants'; import { serialize } from './utils'; @@ -15,7 +15,7 @@ export function serializeError(error: unknown) { }); const docUrl = new URL('https://docs.cloud.coinbase.com/wallet-sdk/docs/errors'); - docUrl.searchParams.set('version', LIB_VERSION); + docUrl.searchParams.set('version', VERSION); docUrl.searchParams.set('code', serialized.code.toString()); docUrl.searchParams.set('message', serialized.message); diff --git a/packages/wallet-sdk/src/createCoinbaseWalletSDK.test.ts b/packages/wallet-sdk/src/createCoinbaseWalletSDK.test.ts index fab27c2096..bf490ac171 100644 --- a/packages/wallet-sdk/src/createCoinbaseWalletSDK.test.ts +++ b/packages/wallet-sdk/src/createCoinbaseWalletSDK.test.ts @@ -7,7 +7,7 @@ const options: CreateCoinbaseWalletSDKOptions = { preference: { options: 'all' }, }; -jest.mock('./util/crossOriginOpenerPolicy'); +jest.mock('./util/checkCrossOriginOpenerPolicy'); describe('createCoinbaseWalletSDK', () => { it('should return an object with a getProvider method', () => { diff --git a/packages/wallet-sdk/src/createCoinbaseWalletSDK.ts b/packages/wallet-sdk/src/createCoinbaseWalletSDK.ts index 45f42ddea1..e48c5726ae 100644 --- a/packages/wallet-sdk/src/createCoinbaseWalletSDK.ts +++ b/packages/wallet-sdk/src/createCoinbaseWalletSDK.ts @@ -1,5 +1,5 @@ import { createCoinbaseWalletProvider } from './createCoinbaseWalletProvider'; -import { LIB_VERSION } from './version'; +import { VERSION } from './sdk-info'; import { AppMetadata, ConstructorOptions, @@ -7,7 +7,7 @@ import { ProviderInterface, } from ':core/provider/interface'; import { ScopedLocalStorage } from ':core/storage/ScopedLocalStorage'; -import { checkCrossOriginOpenerPolicy } from ':util/crossOriginOpenerPolicy'; +import { checkCrossOriginOpenerPolicy } from ':util/checkCrossOriginOpenerPolicy'; import { validatePreferences } from ':util/validatePreferences'; export type CreateCoinbaseWalletSDKOptions = Partial & { @@ -25,7 +25,7 @@ const DEFAULT_PREFERENCE: Preference = { */ export function createCoinbaseWalletSDK(params: CreateCoinbaseWalletSDKOptions) { const versionStorage = new ScopedLocalStorage('CBWSDK'); - versionStorage.setItem('VERSION', LIB_VERSION); + versionStorage.setItem('VERSION', VERSION); void checkCrossOriginOpenerPolicy(); diff --git a/packages/wallet-sdk/src/sdk-info.ts b/packages/wallet-sdk/src/sdk-info.ts new file mode 100644 index 0000000000..e6a4e08a5d --- /dev/null +++ b/packages/wallet-sdk/src/sdk-info.ts @@ -0,0 +1,2 @@ +export const VERSION = '4.1.0'; +export const NAME = '@coinbase/wallet-sdk'; diff --git a/packages/wallet-sdk/src/util/crossOriginOpenerPolicy.test.ts b/packages/wallet-sdk/src/util/checkCrossOriginOpenerPolicy.test.ts similarity index 63% rename from packages/wallet-sdk/src/util/crossOriginOpenerPolicy.test.ts rename to packages/wallet-sdk/src/util/checkCrossOriginOpenerPolicy.test.ts index 6fbfee569b..467388c7d6 100644 --- a/packages/wallet-sdk/src/util/crossOriginOpenerPolicy.test.ts +++ b/packages/wallet-sdk/src/util/checkCrossOriginOpenerPolicy.test.ts @@ -1,4 +1,7 @@ -import { checkCrossOriginOpenerPolicy } from './crossOriginOpenerPolicy'; +import { + checkCrossOriginOpenerPolicy, + getCrossOriginOpenerPolicy, +} from './checkCrossOriginOpenerPolicy'; describe('checkCrossOriginOpenerPolicy', () => { beforeEach(() => { @@ -6,27 +9,33 @@ describe('checkCrossOriginOpenerPolicy', () => { jest.clearAllMocks(); }); - it('should not run if window is undefined', () => { + it('should return non-browser-env if window is undefined', async () => { const originalWindow = global.window; // @ts-expect-error delete window property delete global.window; - expect(checkCrossOriginOpenerPolicy()).toBeUndefined(); + await checkCrossOriginOpenerPolicy(); + + expect(getCrossOriginOpenerPolicy()).toBe('non-browser-env'); // Restore the original window object global.window = originalWindow; }); - it('should fetch the current origin', async () => { + it('should fetch the current origin and pathname', async () => { global.fetch = jest.fn().mockResolvedValue({ headers: { get: jest.fn().mockReturnValue(null), }, + ok: true, }); checkCrossOriginOpenerPolicy(); - expect(global.fetch).toHaveBeenCalledWith(window.location.origin, {}); + expect(global.fetch).toHaveBeenCalledWith( + `${window.location.origin}${window.location.pathname}`, + { method: 'HEAD' } + ); }); it('should log an error if Cross-Origin-Opener-Policy is same-origin', async () => { @@ -35,29 +44,35 @@ describe('checkCrossOriginOpenerPolicy', () => { headers: { get: jest.fn().mockReturnValue('same-origin'), }, + ok: true, }); await checkCrossOriginOpenerPolicy(); + const result = getCrossOriginOpenerPolicy(); expect(consoleErrorSpy).toHaveBeenCalledWith( expect.stringContaining( "Coinbase Wallet SDK requires the Cross-Origin-Opener-Policy header to not be set to 'same-origin'." ) ); + expect(result).toBe('same-origin'); consoleErrorSpy.mockRestore(); }); - it('should not log an error if Cross-Origin-Opener-Policy is not same-origin', async () => { + it('should return true and not log an error if Cross-Origin-Opener-Policy is not same-origin', async () => { const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); global.fetch = jest.fn().mockResolvedValue({ headers: { get: jest.fn().mockReturnValue('unsafe-none'), }, + ok: true, }); await checkCrossOriginOpenerPolicy(); + const result = getCrossOriginOpenerPolicy(); expect(consoleErrorSpy).not.toHaveBeenCalled(); + expect(result).toBe('unsafe-none'); consoleErrorSpy.mockRestore(); }); }); diff --git a/packages/wallet-sdk/src/util/checkCrossOriginOpenerPolicy.ts b/packages/wallet-sdk/src/util/checkCrossOriginOpenerPolicy.ts new file mode 100644 index 0000000000..41b4f5c957 --- /dev/null +++ b/packages/wallet-sdk/src/util/checkCrossOriginOpenerPolicy.ts @@ -0,0 +1,62 @@ +const COOP_ERROR_MESSAGE = `Coinbase Wallet SDK requires the Cross-Origin-Opener-Policy header to not be set to 'same-origin'. This is to ensure that the SDK can communicate with the Coinbase Smart Wallet app. + +Please see https://www.smartwallet.dev/guides/tips/popup-tips#cross-origin-opener-policy for more information.`; + +/** + * Creates a checker for the Cross-Origin-Opener-Policy (COOP). + * + * @returns An object with methods to get and check the Cross-Origin-Opener-Policy. + * + * @method getCrossOriginOpenerPolicy + * Retrieves current Cross-Origin-Opener-Policy. + * @throws Will throw an error if the policy has not been checked yet. + * + * @method checkCrossOriginOpenerPolicy + * Checks the Cross-Origin-Opener-Policy of the current environment. + * If in a non-browser environment, sets the policy to 'non-browser-env'. + * If in a browser environment, fetches the policy from the current origin. + * Logs an error if the policy is 'same-origin'. + */ +const createCoopChecker = () => { + let crossOriginOpenerPolicy: string | undefined; + + return { + getCrossOriginOpenerPolicy: () => { + if (crossOriginOpenerPolicy === undefined) { + return 'undefined'; + } + + return crossOriginOpenerPolicy; + }, + checkCrossOriginOpenerPolicy: async () => { + if (typeof window === 'undefined') { + // Non-browser environment + crossOriginOpenerPolicy = 'non-browser-env'; + return; + } + + try { + const url = `${window.location.origin}${window.location.pathname}`; + const response = await fetch(url, { + method: 'HEAD', + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = response.headers.get('Cross-Origin-Opener-Policy'); + crossOriginOpenerPolicy = result ?? 'null'; + + if (crossOriginOpenerPolicy === 'same-origin') { + console.error(COOP_ERROR_MESSAGE); + } + } catch (error) { + console.error('Error checking Cross-Origin-Opener-Policy:', (error as Error).message); + crossOriginOpenerPolicy = 'error'; + } + }, + }; +}; + +export const { checkCrossOriginOpenerPolicy, getCrossOriginOpenerPolicy } = createCoopChecker(); diff --git a/packages/wallet-sdk/src/util/crossOriginOpenerPolicy.ts b/packages/wallet-sdk/src/util/crossOriginOpenerPolicy.ts deleted file mode 100644 index 0d9e282ba0..0000000000 --- a/packages/wallet-sdk/src/util/crossOriginOpenerPolicy.ts +++ /dev/null @@ -1,15 +0,0 @@ -export function checkCrossOriginOpenerPolicy() { - if (typeof window === 'undefined') { - return; - } - - fetch(window.location.origin, {}).then((response) => { - const headers = response.headers; - const crossOriginOpenerPolicy = headers.get('Cross-Origin-Opener-Policy'); - if (crossOriginOpenerPolicy === 'same-origin') { - console.error(`Coinbase Wallet SDK requires the Cross-Origin-Opener-Policy header to not be set to 'same-origin'. This is to ensure that the SDK can communicate with the Coinbase Smart Wallet app. - -Please see https://www.smartwallet.dev/guides/tips/popup-tips#cross-origin-opener-policy for more information.`); - } - }); -} diff --git a/packages/wallet-sdk/src/util/provider.ts b/packages/wallet-sdk/src/util/provider.ts index 9ea783ac0f..ed3239cfe9 100644 --- a/packages/wallet-sdk/src/util/provider.ts +++ b/packages/wallet-sdk/src/util/provider.ts @@ -1,4 +1,4 @@ -import { LIB_VERSION } from '../version'; +import { NAME, VERSION } from '../sdk-info'; import { standardErrors } from ':core/error'; import { ConstructorOptions, ProviderInterface, RequestArguments } from ':core/provider/interface'; @@ -12,7 +12,11 @@ export async function fetchRPCRequest(request: RequestArguments, rpcUrl: string) method: 'POST', body: JSON.stringify(requestBody), mode: 'cors', - headers: { 'Content-Type': 'application/json', 'X-Cbw-Sdk-Version': LIB_VERSION }, + headers: { + 'Content-Type': 'application/json', + 'X-Cbw-Sdk-Version': VERSION, + 'X-Cbw-Sdk-Platform': NAME, + }, }); const { result, error } = await res.json(); if (error) throw error; diff --git a/packages/wallet-sdk/src/util/web.test.ts b/packages/wallet-sdk/src/util/web.test.ts index cbfd4ec40f..dacfd7a5bf 100644 --- a/packages/wallet-sdk/src/util/web.test.ts +++ b/packages/wallet-sdk/src/util/web.test.ts @@ -1,6 +1,14 @@ +import { NAME, VERSION } from 'src/sdk-info'; + +import { getCrossOriginOpenerPolicy } from './checkCrossOriginOpenerPolicy'; import { closePopup, openPopup } from './web'; import { standardErrors } from ':core/error'; +jest.mock('./checkCrossOriginOpenerPolicy'); +(getCrossOriginOpenerPolicy as jest.Mock).mockReturnValue('null'); + +const mockOrigin = 'http://localhost'; + describe('PopupManager', () => { beforeAll(() => { global.window = Object.create(window); @@ -11,6 +19,7 @@ describe('PopupManager', () => { screenY: { value: 0 }, open: { value: jest.fn() }, close: { value: jest.fn() }, + location: { value: { origin: mockOrigin } }, }); }); @@ -31,6 +40,11 @@ describe('PopupManager', () => { 'width=420, height=540, left=302, top=114' ); expect(popup.focus).toHaveBeenCalledTimes(1); + + expect(url.searchParams.get('sdkName')).toBe(NAME); + expect(url.searchParams.get('sdkVersion')).toBe(VERSION); + expect(url.searchParams.get('origin')).toBe(mockOrigin); + expect(url.searchParams.get('coop')).toBe('null'); }); it('should throw an error if popup fails to open', () => { diff --git a/packages/wallet-sdk/src/util/web.ts b/packages/wallet-sdk/src/util/web.ts index 70b8032f9e..982c121446 100644 --- a/packages/wallet-sdk/src/util/web.ts +++ b/packages/wallet-sdk/src/util/web.ts @@ -1,3 +1,5 @@ +import { NAME, VERSION } from '../sdk-info'; +import { getCrossOriginOpenerPolicy } from './checkCrossOriginOpenerPolicy'; import { standardErrors } from ':core/error'; const POPUP_WIDTH = 420; @@ -9,15 +11,20 @@ export function openPopup(url: URL): Window { const left = (window.innerWidth - POPUP_WIDTH) / 2 + window.screenX; const top = (window.innerHeight - POPUP_HEIGHT) / 2 + window.screenY; + appendAppInfoQueryParams(url); + const popup = window.open( url, 'Smart Wallet', `width=${POPUP_WIDTH}, height=${POPUP_HEIGHT}, left=${left}, top=${top}` ); + popup?.focus(); + if (!popup) { throw standardErrors.rpc.internal('Pop up window failed to open'); } + return popup; } @@ -27,8 +34,15 @@ export function closePopup(popup: Window | null) { } } -/** - * TODO: consolidate all UI related helper functions, - * ones making window.xxx() document.yyy() calls. - * e.g. WLMobileRelayUI, WalletLinkRelay, ... - */ +function appendAppInfoQueryParams(url: URL) { + const params = { + sdkName: NAME, + sdkVersion: VERSION, + origin: window.location.origin, + coop: getCrossOriginOpenerPolicy(), + }; + + for (const [key, value] of Object.entries(params)) { + url.searchParams.append(key, value.toString()); + } +} diff --git a/packages/wallet-sdk/src/version.ts b/packages/wallet-sdk/src/version.ts deleted file mode 100644 index c2a0f8b068..0000000000 --- a/packages/wallet-sdk/src/version.ts +++ /dev/null @@ -1 +0,0 @@ -export const LIB_VERSION = '4.1.0';