diff --git a/package.json b/package.json index 1e01f91e2..fc214779c 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "packages/devreactnative", "packages/devnext", "packages/deve2e", - "packages/playground-next" + "packages/playground-next", + "packages/sdk-multichain" ], "scripts": { "build": "yarn install && cd packages/sdk-socket-server-next && yarn install && cd ../.. && yarn workspaces foreach --verbose run build:pre-tsc && yarn workspaces foreach --verbose --topological --parallel --no-private run build && yarn workspaces foreach --verbose run build:post-tsc ", diff --git a/packages/sdk-multichain/CHANGELOG.md b/packages/sdk-multichain/CHANGELOG.md new file mode 100644 index 000000000..3cab0227d --- /dev/null +++ b/packages/sdk-multichain/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/MetaMask/metamask-sdk/ diff --git a/packages/sdk-multichain/README.md b/packages/sdk-multichain/README.md new file mode 100644 index 000000000..f2bee92db --- /dev/null +++ b/packages/sdk-multichain/README.md @@ -0,0 +1,22 @@ +# MetaMask SDK Multichain + +The MetaMask SDK Multichain is a protocol-based, domain-driven SDK that enables seamless integration with MetaMask wallets across multiple blockchain networks and platforms. + +## Overview + +The SDK provides a unified interface for dapps to connect with MetaMask Extension or Mobile wallets, regardless of the platform (browser, mobile, or Node.js) or blockchain protocol. It automatically handles connection flows, deeplinks, and QR codes based on the user's environment. + +## Architecture + +### Domain-Driven Design + +The SDK follows a clean domain-driven architecture with clear separation of concerns: + +``` +src/ +├── domain/ # Core business logic and abstractions +│ ├── multichain/ # Multichain protocol abstractions +│ ├── events/ # Event-driven communication +│ ├── platform/ # Platform detection utilities +│ └── store/ # Storage abstractions +``` diff --git a/packages/sdk-multichain/biome.json b/packages/sdk-multichain/biome.json new file mode 100644 index 000000000..398c34da9 --- /dev/null +++ b/packages/sdk-multichain/biome.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.0.0/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "includes": ["**", "!**/node_modules/**", "!**/dist/**"], + "ignoreUnknown": false + }, + "formatter": { + "enabled": true, + "indentStyle": "tab", + "lineWidth": 180 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} \ No newline at end of file diff --git a/packages/sdk-multichain/package.json b/packages/sdk-multichain/package.json new file mode 100644 index 000000000..395923983 --- /dev/null +++ b/packages/sdk-multichain/package.json @@ -0,0 +1,59 @@ +{ + "name": "@metamask/multichain-sdk", + "version": "0.0.0", + "description": "Multichain package for MetaMask SDK", + "main": "dist/node/cjs/multichain-sdk.js", + "module": "dist/browser/es/multichain-sdk.mjs", + "browser": "dist/browser/es/multichain-sdk.mjs", + "unpkg": "dist/browser/umd/multichain-sdk.js", + "react-native": "dist/react-native/es/multichain-sdk.mjs", + "types": "dist/types/multichain-sdk.d.ts", + "sideEffects": false, + "files": [ + "/dist" + ], + "scripts": { + "clean": "npx rimraf ./dist", + "build:types": "tsc --project tsconfig.json --emitDeclarationOnly --declaration --outFile", + "build": "yarn clean && npx tsup", + "build:post-tsc": "echo 'N/A'", + "build:pre-tsc": "echo 'N/A'", + "lint": "yarn biome lint ./src", + "lint:ci": "yarn biome ci ./src", + "lint:fix": "yarn biome format --write ./src", + "lint:changelog": "../../scripts/validate-changelog.sh @metamask/multichain-sdk", + "test": "vitest run", + "test:unit": "vitest run", + "test:watch": "vitest watch", + "test:ci": "vitest run --coverage --coverage.reporter=text --silent", + "allow-scripts": "" + }, + "devDependencies": { + "@biomejs/biome": "2.0.0", + "@metamask/auto-changelog": "^3.4.3", + "@react-native-async-storage/async-storage": "^1.19.6", + "@vitest/coverage-v8": "^3.2.4", + "esbuild-plugin-umd-wrapper": "^3.0.0", + "nock": "^14.0.4", + "prettier": "^3.3.3", + "tsup": "^8.5.0", + "typescript": "^5.8.3", + "typescript-eslint": "^8.6.0", + "vitest": "^3.1.2" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "license": "MIT", + "dependencies": { + "@metamask/multichain-api-client": "^0.6.4", + "@metamask/sdk-analytics": "workspace:^", + "@metamask/utils": "^11.4.0", + "@paulmillr/qr": "^0.2.1", + "bowser": "^2.11.0", + "cross-fetch": "^4.1.0", + "eventemitter2": "^6.4.9", + "uuid": "^11.1.0" + } +} diff --git a/packages/sdk-multichain/src/domain/events/index.ts b/packages/sdk-multichain/src/domain/events/index.ts new file mode 100644 index 000000000..cb3c95584 --- /dev/null +++ b/packages/sdk-multichain/src/domain/events/index.ts @@ -0,0 +1,77 @@ +import { EventEmitter2 } from 'eventemitter2'; + +import type { EventTypes } from './types'; + + +/** + * A type-safe event emitter that provides a strongly-typed wrapper around EventEmitter2. + * + * This class ensures type safety for event names and their corresponding argument types, + * making it easier to work with events in a type-safe manner. + * + * @template TEvents - A record type mapping event names to their argument types. + * Each key represents an event name, and the value is a tuple of argument types. + */ +export class EventEmitter< + TEvents extends Record = EventTypes, +> { + readonly #emitter = new EventEmitter2(); + + /** + * Emits an event with the specified name and arguments. + * + * @template TEventName - The name of the event to emit (must be a key of TEvents) + * @param eventName - The name of the event to emit + * @param eventArg - The arguments to pass to the event handlers + */ + emit( + eventName: TEventName, + ...eventArg: TEvents[TEventName] + ) { + this.#emitter.emit(eventName, ...eventArg); + } + + /** + * Registers an event handler for the specified event. + * + * @template TEventName - The name of the event to listen for (must be a key of TEvents) + * @param eventName - The name of the event to listen for + * @param handler - The function to call when the event is emitted + */ + on( + eventName: TEventName, + handler: (...eventArg: TEvents[TEventName]) => void, + ) { + this.#emitter.on(eventName, handler); + } + + /** + * Sets the maximum number of listeners that can be registered for any single event. + * + * This is useful for preventing memory leaks when many listeners are registered. + * By default, EventEmitter2 will warn if more than 10 listeners are registered + * for a single event. + * + * @param maxListeners - The maximum number of listeners per event (0 means unlimited) + */ + setMaxListeners(maxListeners: number) { + this.#emitter.setMaxListeners(maxListeners); + } + + /** + * Removes a specific event handler for the specified event. + * + * @template TEventName - The name of the event to remove the handler from (must be a key of TEvents) + * @param eventName - The name of the event to remove the handler from + * @param handler - The specific handler function to remove + */ + off( + eventName: TEventName, + handler: (...eventArg: TEvents[TEventName]) => void, + ) { + this.#emitter.off(eventName, handler); + } +} + + +export type * from './types'; diff --git a/packages/sdk-multichain/src/domain/events/types/extension.ts b/packages/sdk-multichain/src/domain/events/types/extension.ts new file mode 100644 index 000000000..08de33381 --- /dev/null +++ b/packages/sdk-multichain/src/domain/events/types/extension.ts @@ -0,0 +1,6 @@ +/** + * Extension native Events + */ +export type ExtensionEvents = { + chainChanged: [evt: unknown]; +}; diff --git a/packages/sdk-multichain/src/domain/events/types/index.ts b/packages/sdk-multichain/src/domain/events/types/index.ts new file mode 100644 index 000000000..1ff8a790d --- /dev/null +++ b/packages/sdk-multichain/src/domain/events/types/index.ts @@ -0,0 +1,5 @@ +import type { ExtensionEvents } from './extension'; +import type { SDKEvents } from './sdk'; + +export type EventTypes = SDKEvents | ExtensionEvents; +export type { SDKEvents, ExtensionEvents }; diff --git a/packages/sdk-multichain/src/domain/events/types/sdk.ts b/packages/sdk-multichain/src/domain/events/types/sdk.ts new file mode 100644 index 000000000..46197c788 --- /dev/null +++ b/packages/sdk-multichain/src/domain/events/types/sdk.ts @@ -0,0 +1,3 @@ +export type SDKEvents = { + display_uri: [evt: string]; +}; diff --git a/packages/sdk-multichain/src/domain/index.test.ts b/packages/sdk-multichain/src/domain/index.test.ts new file mode 100644 index 000000000..a23f139b6 --- /dev/null +++ b/packages/sdk-multichain/src/domain/index.test.ts @@ -0,0 +1,515 @@ + + +import * as t from 'vitest' +import Bowser from 'bowser'; + +import { EventEmitter,ExtensionEvents, SDKEvents, getPlatformType, PlatformType, createLogger, enableDebug, isEnabled } from './'; + + + + +const parseMock = t.vi.fn(); +// Mock Bowser at the top level +t.vi.mock('bowser', () => ({ + default: { + parse: parseMock, + }, +})); + +t.describe('Platform Detection', () => { + let mockBowser: { parse: t.MockedFunction }; + + t.beforeEach(() => { + // Reset mocks + t.vi.clearAllMocks(); + t.vi.unstubAllGlobals(); + // Get the mocked instance - Bowser is a default export + mockBowser = t.vi.mocked(Bowser); + }); + + t.describe('getPlatformType', () => { + t.it('should return ReactNative when environment is React Native', () => { + // Mock React Native environment + t.vi.stubGlobal('window', { + navigator: { + product: 'ReactNative', + userAgent: 'ReactNative', + }, + }); + t.vi.stubGlobal('navigator', { + product: 'ReactNative', + userAgent: 'ReactNative', + }); + + const result = getPlatformType(); + t.expect(result).toBe(PlatformType.ReactNative); + }); + + t.it('should return NonBrowser when window is undefined', () => { + t.vi.stubGlobal('window', undefined); + t.vi.stubGlobal('navigator', undefined); + + const result = getPlatformType(); + t.expect(result).toBe(PlatformType.NonBrowser); + }); + + t.it('should return NonBrowser when window.navigator is undefined', () => { + t.vi.stubGlobal('window', {}); + t.vi.stubGlobal('navigator', undefined); + + const result = getPlatformType(); + t.expect(result).toBe(PlatformType.NonBrowser); + }); + + t.it('should return MetaMaskMobileWebview when in MetaMask mobile webview', () => { + t.vi.stubGlobal('window', { + ReactNativeWebView: {}, + navigator: { + userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1 MetaMaskMobile', + }, + }); + t.vi.stubGlobal('navigator', { + userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1 MetaMaskMobile', + }); + + const result = getPlatformType(); + t.expect(result).toBe(PlatformType.MetaMaskMobileWebview); + }); + + t.it('should return MobileWeb when platform type is mobile', () => { + const mockBrowserInfo = { + platform: { type: 'mobile' }, + }; + + t.vi.stubGlobal('window', { + navigator: { + userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15', + }, + }); + t.vi.stubGlobal('navigator', { + userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15', + }); + + parseMock.mockReturnValue(mockBrowserInfo); + + const result = getPlatformType(); + t.expect(result).toBe(PlatformType.MobileWeb); + }); + + t.it('should return MobileWeb when platform type is tablet', () => { + const mockBrowserInfo = { + platform: { type: 'tablet' }, + }; + + t.vi.stubGlobal('window', { + navigator: { + userAgent: 'Mozilla/5.0 (iPad; CPU OS 15_0 like Mac OS X) AppleWebKit/605.1.15', + }, + }); + t.vi.stubGlobal('navigator', { + userAgent: 'Mozilla/5.0 (iPad; CPU OS 15_0 like Mac OS X) AppleWebKit/605.1.15', + }); + + parseMock.mockReturnValue(mockBrowserInfo); + + const result = getPlatformType(); + t.expect(result).toBe(PlatformType.MobileWeb); + }); + + t.it('should return DesktopWeb when platform type is desktop', () => { + const mockBrowserInfo = { + platform: { type: 'desktop' }, + }; + + t.vi.stubGlobal('window', { + navigator: { + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + }, + }); + t.vi.stubGlobal('navigator', { + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + }); + + parseMock.mockReturnValue(mockBrowserInfo); + + const result = getPlatformType(); + t.expect(result).toBe(PlatformType.DesktopWeb); + }); + + t.it('should return DesktopWeb when platform type is undefined', () => { + const mockBrowserInfo = { + platform: { type: undefined }, + }; + + t.vi.stubGlobal('window', { + navigator: { + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + }, + }); + t.vi.stubGlobal('navigator', { + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + }); + + parseMock.mockReturnValue(mockBrowserInfo); + + const result = getPlatformType(); + t.expect(result).toBe(PlatformType.DesktopWeb); + }); + + t.it('should not return MetaMaskMobileWebview when ReactNativeWebView is missing', () => { + t.vi.stubGlobal('window', { + navigator: { + userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1 MetaMaskMobile', + }, + }); + t.vi.stubGlobal('navigator', { + userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1 MetaMaskMobile', + }); + + const mockBrowserInfo = { + platform: { type: 'mobile' }, + }; + parseMock.mockReturnValue(mockBrowserInfo); + + const result = getPlatformType(); + t.expect(result).toBe(PlatformType.MobileWeb); + }); + + t.it('should not return MetaMaskMobileWebview when userAgent does not end with MetaMaskMobile', () => { + t.vi.stubGlobal('window', { + ReactNativeWebView: {}, + navigator: { + userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15', + }, + }); + t.vi.stubGlobal('navigator', { + userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15', + }); + + const mockBrowserInfo = { + platform: { type: 'mobile' }, + }; + parseMock.mockReturnValue(mockBrowserInfo); + + const result = getPlatformType(); + t.expect(result).toBe(PlatformType.MobileWeb); + }); + + t.it('should handle global navigator.product being ReactNative', () => { + t.vi.stubGlobal('global', { + navigator: { + product: 'ReactNative', + }, + }); + t.vi.stubGlobal('navigator', { + product: 'ReactNative', + }); + + const result = getPlatformType(); + t.expect(result).toBe(PlatformType.NonBrowser); + }); + }); +}); + +t.describe("Logger", () => { + + t.vi.mock('debug', { spy: true }); + + // Mock StoreClient + const mockStoreClient = { + getDebug: t.vi.fn(), + }; + + t.beforeEach(() => { + t.vi.clearAllMocks(); + delete process.env.DEBUG; + mockStoreClient.getDebug.mockClear(); + }); + + t.describe('createLogger', () => { + t.it('should create a logger with default namespace', () => { + const logger = createLogger(); + + t.expect(logger).toBeDefined(); + t.expect(typeof logger).toBe('function'); + }); + + t.it('should create a logger with custom namespace', () => { + const logger = createLogger('metamask-sdk:core'); + + t.expect(logger).toBeDefined(); + t.expect(typeof logger).toBe('function'); + }); + + t.it('should create a logger with custom namespace and color', () => { + const logger = createLogger('metamask-sdk:core', '123'); + + t.expect(logger).toBeDefined(); + t.expect(typeof logger).toBe('function'); + t.expect(logger.color).toBe('123'); + }); + }); + + t.describe('enableDebug', () => { + t.it('should enable debug with default namespace', () => { + t.expect(() => enableDebug()).not.toThrow(); + }); + + t.it('should enable debug with custom namespace', () => { + t.expect(() => enableDebug('metamask-sdk:provider')).not.toThrow(); + }); + }); + + t.describe('isEnabled', () => { + t.it('should return true when namespace is in process.env.DEBUG', async () => { + process.env.DEBUG = 'metamask-sdk:core'; + + const result = await isEnabled('metamask-sdk:core', mockStoreClient as any); + + t.expect(result).toBe(true); + t.expect(mockStoreClient.getDebug).not.toHaveBeenCalled(); + }); + + t.it('should return true when wildcard matches namespace', async () => { + process.env.DEBUG = 'metamask-sdk:*'; + + const result = await isEnabled('metamask-sdk:core', mockStoreClient as any); + + t.expect(result).toBe(true); + }); + + t.it('should return true when universal wildcard is used', async () => { + process.env.DEBUG = '*'; + + const result = await isEnabled('metamask-sdk', mockStoreClient as any); + + t.expect(result).toBe(true); + }); + + t.it('should return false when namespace is not in process.env.DEBUG', async () => { + process.env.DEBUG = 'other-namespace'; + + const result = await isEnabled('metamask-sdk:core', mockStoreClient as any); + + t.expect(result).toBe(false); + }); + + t.it('should check storage when process.env.DEBUG is not set', async () => { + mockStoreClient.getDebug.mockResolvedValue('metamask-sdk:provider'); + + const result = await isEnabled('metamask-sdk:provider', mockStoreClient as any); + + t.expect(result).toBe(true); + t.expect(mockStoreClient.getDebug).toHaveBeenCalled(); + }); + + t.it('should return false when storage debug is empty', async () => { + mockStoreClient.getDebug.mockResolvedValue(null); + + const result = await isEnabled('metamask-sdk', mockStoreClient as any); + + t.expect(result).toBe(false); + }); + + t.it('should prioritize process.env.DEBUG over storage', async () => { + process.env.DEBUG = 'metamask-sdk:core'; + mockStoreClient.getDebug.mockResolvedValue('metamask-sdk:provider'); + + const result = await isEnabled('metamask-sdk:core', mockStoreClient as any); + + t.expect(result).toBe(true); + t.expect(mockStoreClient.getDebug).not.toHaveBeenCalled(); + }); + }); + +}) + + +t.describe('EventEmitter', () => { + + + t.describe('emit', () => { + + t.describe("SDKEvents", () => { + let eventEmitter: EventEmitter; + + t.beforeEach(() => { + eventEmitter = new EventEmitter(); + }); + + t.it('should emit display_uri event with string argument', () => { + const handler = t.vi.fn(); + eventEmitter.on('display_uri', handler); + + eventEmitter.emit('display_uri', 'uri://test-connection'); + + t.expect(handler).toHaveBeenCalledWith('uri://test-connection'); + t.expect(handler).toHaveBeenCalledTimes(1); + }); + + t.it('should emit to all registered handlers for the same event', () => { + const handler1 = t.vi.fn(); + const handler2 = t.vi.fn(); + const handler3 = t.vi.fn(); + + eventEmitter.on('display_uri', handler1); + eventEmitter.on('display_uri', handler2); + eventEmitter.on('display_uri', handler3); + + eventEmitter.emit('display_uri', 'broadcast-uri'); + + t.expect(handler1).toHaveBeenCalledWith('broadcast-uri'); + t.expect(handler2).toHaveBeenCalledWith('broadcast-uri'); + t.expect(handler3).toHaveBeenCalledWith('broadcast-uri'); + t.expect(handler1).toHaveBeenCalledTimes(1); + t.expect(handler2).toHaveBeenCalledTimes(1); + t.expect(handler3).toHaveBeenCalledTimes(1); + }); + + t.it('should register handlers for display_uri event', () => { + const handler = t.vi.fn(); + + eventEmitter.on('display_uri', handler); + eventEmitter.emit('display_uri', 'test-uri'); + + t.expect(handler).toHaveBeenCalledWith('test-uri'); + }); + + t.it('should register multiple handlers for the same event', () => { + const handler1 = t.vi.fn(); + const handler2 = t.vi.fn(); + + eventEmitter.on('display_uri', handler1); + eventEmitter.on('display_uri', handler2); + + eventEmitter.emit('display_uri', 'test'); + + t.expect(handler1).toHaveBeenCalledWith('test'); + t.expect(handler2).toHaveBeenCalledWith('test'); + }); + + t.it('should remove specific handlers for display_uri event', () => { + const handler1 = t.vi.fn(); + const handler2 = t.vi.fn(); + + eventEmitter.on('display_uri', handler1); + eventEmitter.on('display_uri', handler2); + + eventEmitter.off('display_uri', handler1); + eventEmitter.emit('display_uri', 'test'); + + t.expect(handler1).not.toHaveBeenCalled(); + t.expect(handler2).toHaveBeenCalledWith('test'); + }); + + t.it('should handle removing non-existent handlers gracefully', () => { + const handler = t.vi.fn(); + const nonExistentHandler = t.vi.fn(); + + eventEmitter.on('display_uri', handler); + + // This should not throw an error + t.expect(() => { + eventEmitter.off('display_uri', nonExistentHandler); + }).not.toThrow(); + + eventEmitter.emit('display_uri', 'still works'); + t.expect(handler).toHaveBeenCalledWith('still works'); + }); + + t.it('should handle removing handlers from non-existent events gracefully', () => { + const handler = t.vi.fn(); + + // This should not throw an error + t.expect(() => { + eventEmitter.off('display_uri', handler); + }).not.toThrow(); + }); + t.it('should not leak memory when handlers are removed', () => { + const handlers: Array<() => void> = []; + + // Set a higher limit to avoid warnings + eventEmitter.setMaxListeners(200); + + // Add many handlers + for (let i = 0; i < 100; i++) { + const handler = t.vi.fn(); + handlers.push(handler); + eventEmitter.on('display_uri', handler); + } + + // Remove all handlers + handlers.forEach(handler => { + eventEmitter.off('display_uri', handler); + }); + + // Emit should not call any handlers + eventEmitter.emit('display_uri', 'should not be handled'); + + handlers.forEach(handler => { + t.expect(handler).not.toHaveBeenCalled(); + }); + }); + }) + + t.describe("ExtensionEvents", () => { + let eventEmitter: EventEmitter; + t.beforeEach(() => { + eventEmitter = new EventEmitter(); + }); + + t.it('should emit chainChanged event with unknown argument', () => { + const handler = t.vi.fn(); + eventEmitter.on('chainChanged', handler); + + const chainData = { chainId: '0x1', networkName: 'mainnet' }; + eventEmitter.emit('chainChanged', chainData); + + t.expect(handler).toHaveBeenCalledWith(chainData); + t.expect(handler).toHaveBeenCalledTimes(1); + }); + + t.it('should emit chainChanged event with various data types', () => { + const handler = t.vi.fn(); + eventEmitter.on('chainChanged', handler); + + // Test with string + eventEmitter.emit('chainChanged', 'chain-id'); + t.expect(handler).toHaveBeenCalledWith('chain-id'); + + // Test with number + eventEmitter.emit('chainChanged', 1); + t.expect(handler).toHaveBeenCalledWith(1); + + // Test with null + eventEmitter.emit('chainChanged', null); + t.expect(handler).toHaveBeenCalledWith(null); + + t.expect(handler).toHaveBeenCalledTimes(3); + }); + + t.it('should register handlers for chainChanged event', () => { + const handler = t.vi.fn(); + + eventEmitter.on('chainChanged', handler); + eventEmitter.emit('chainChanged', { chainId: '0x89' }); + + t.expect(handler).toHaveBeenCalledWith({ chainId: '0x89' }); + }); + + t.it('should remove specific handlers for chainChanged event', () => { + const handler1 = t.vi.fn(); + const handler2 = t.vi.fn(); + + eventEmitter.on('chainChanged', handler1); + eventEmitter.on('chainChanged', handler2); + + eventEmitter.off('chainChanged', handler1); + eventEmitter.emit('chainChanged', { chainId: '0x1' }); + + t.expect(handler1).not.toHaveBeenCalled(); + t.expect(handler2).toHaveBeenCalledWith({ chainId: '0x1' }); + }); + }) + }); +}); diff --git a/packages/sdk-multichain/src/domain/index.ts b/packages/sdk-multichain/src/domain/index.ts new file mode 100644 index 000000000..7fbea409d --- /dev/null +++ b/packages/sdk-multichain/src/domain/index.ts @@ -0,0 +1,5 @@ +export * from './platform'; +export * from './multichain'; +export * from './events'; +export * from './logger'; +export * from './store'; diff --git a/packages/sdk-multichain/src/domain/logger/index.ts b/packages/sdk-multichain/src/domain/logger/index.ts new file mode 100644 index 000000000..2f3607116 --- /dev/null +++ b/packages/sdk-multichain/src/domain/logger/index.ts @@ -0,0 +1,91 @@ +import debug from 'debug'; + +import type { StoreClient } from '../store/client'; + +/** + * Supported debug namespace types for the MetaMask SDK logger. + * These namespaces help categorize and filter debug output. + */ +export type LoggerNameSpaces = + | 'metamask-sdk' + | 'metamask-sdk:core' + | 'metamask-sdk:provider'; + +/** + * Creates a debug logger instance with the specified namespace and color. + * + * This function initializes a debug logger using the 'debug' library, + * which allows for conditional logging based on environment variables or storage settings. + * + * @param namespace - The debug namespace to use for this logger instance + * @param color - The ANSI color code to use for log output (default: '214' for yellow) + * @returns A configured debug logger instance + */ +export const createLogger = ( + namespace: LoggerNameSpaces = 'metamask-sdk', + color = '214', +) => { + const logger = debug(namespace); + logger.color = color; // Yellow color (basic ANSI) + return logger; +}; + +/** + * Enables debug logging for the specified namespace. + * + * This function activates debug output for the given namespace, + * allowing debug messages to be displayed in the console. + * + * @param namespace - The debug namespace to enable + */ +export const enableDebug = (namespace: LoggerNameSpaces = 'metamask-sdk') => { + debug.enable(namespace); +}; + +/** + * Checks if a specific namespace is enabled in the given debug value string. + * + * This function determines whether debug logging should be active for a namespace + * by checking if the debug value contains the namespace, a wildcard pattern, or + * the general MetaMask SDK wildcard. + * + * @param debugValue - The debug configuration string (e.g., from environment or storage) + * @param namespace - The namespace to check for enablement + * @returns True if the namespace should have debug logging enabled, false otherwise + */ +function isNamespaceEnabled(debugValue: string, namespace: LoggerNameSpaces) { + return ( + debugValue.includes(namespace) || + debugValue.includes('metamask-sdk:*') || + debugValue.includes('*') + ); +} + +/** + * Determines if debug logging is enabled for a specific namespace. + * + * This function checks multiple sources to determine if debug logging should be active: + * 1. First checks the process environment variable 'debug' + * 2. Falls back to checking the debug setting in storage + * 3. Returns false if neither source enables the namespace + * + * @param namespace - The namespace to check for debug enablement + * @param storage - The storage client to check for debug settings + * @returns Promise that resolves to true if debug logging is enabled, false otherwise + */ +export const isEnabled = async ( + namespace: LoggerNameSpaces, + storage: StoreClient, +) => { + if (process?.env?.DEBUG) { + const {DEBUG} = process.env + return isNamespaceEnabled(DEBUG, namespace); + } + + const storageDebug = await storage.getDebug(); + if (storageDebug) { + return isNamespaceEnabled(storageDebug, namespace); + } + + return false; +}; diff --git a/packages/sdk-multichain/src/domain/multichain/api/constants.ts b/packages/sdk-multichain/src/domain/multichain/api/constants.ts new file mode 100644 index 000000000..03689bfb5 --- /dev/null +++ b/packages/sdk-multichain/src/domain/multichain/api/constants.ts @@ -0,0 +1,153 @@ +/* c8 ignore start */ +import { RPC_URLS_MAP } from "./types"; + +export const infuraRpcUrls: RPC_URLS_MAP = { + // ###### Ethereum ###### + // Mainnet + 'eip155:1': 'https://mainnet.infura.io/v3/', + // Goerli + 'eip155:5': 'https://goerli.infura.io/v3/', + // Sepolia 11155111 + 'eip155:11155111': 'https://sepolia.infura.io/v3/', + // ###### Linea ###### + // Mainnet Alpha + 'eip155:59144': 'https://linea-mainnet.infura.io/v3/', + // Testnet ( linea goerli ) + 'eip155:59140': 'https://linea-goerli.infura.io/v3/', + // ###### Polygon ###### + // Mainnet + 'eip155:137': 'https://polygon-mainnet.infura.io/v3/', + // Mumbai + 'eip155:80001': 'https://polygon-mumbai.infura.io/v3/', + // ###### Optimism ###### + // Mainnet + 'eip155:10': 'https://optimism-mainnet.infura.io/v3/', + // Goerli + 'eip155:420': 'https://optimism-goerli.infura.io/v3/', + // ###### Arbitrum ###### + // Mainnet + 'eip155:42161': 'https://arbitrum-mainnet.infura.io/v3/', + // Goerli + 'eip155:421613': 'https://arbitrum-goerli.infura.io/v3/', + // ###### Palm ###### + // Mainnet + 'eip155:11297108109': 'https://palm-mainnet.infura.io/v3/', + // Testnet + 'eip155:11297108099': 'https://palm-testnet.infura.io/v3/', + // ###### Avalanche C-Chain ###### + // Mainnet + 'eip155:43114': 'https://avalanche-mainnet.infura.io/v3/', + // Fuji + 'eip155:43113': 'https://avalanche-fuji.infura.io/v3/', + // // ###### NEAR ###### + // // Mainnet + // 'near:mainnet': `https://near-mainnet.infura.io/v3/`, + // // Testnet + // 'near:testnet': `https://near-testnet.infura.io/v3/`, + // ###### Aurora ###### + // Mainnet + 'eip155:1313161554': 'https://aurora-mainnet.infura.io/v3/', + // Testnet + 'eip155:1313161555': 'https://aurora-testnet.infura.io/v3/', + // ###### StarkNet ###### + // Mainnet + // + // 'starknet:SN_MAIN': `https://starknet-mainnet.infura.io/v3/`, + // // Goerli + // 'starknet:SN_GOERLI': `https://starknet-goerli.infura.io/v3/`, + // // Goerli 2 + // 'starknet:SN_GOERLI2': `https://starknet-goerli2.infura.io/v3/`, + // ###### Celo ###### + // Mainnet + 'eip155:42220': 'https://celo-mainnet.infura.io/v3/', + // Alfajores Testnet + 'eip155:44787': 'https://celo-alfajores.infura.io/v3/', +}; + + + +/** + * Standard RPC method names used in the MetaMask ecosystem. + * + * This constant provides a centralized registry of all supported + * RPC methods, making it easier to reference them consistently + * throughout the codebase. + */ +export const RPC_METHODS = { + /** Get the current provider state */ + METAMASK_GETPROVIDERSTATE: 'metamask_getProviderState', + /** Connect and sign in a single operation */ + METAMASK_CONNECTSIGN: 'metamask_connectSign', + /** Connect with specific parameters */ + METAMASK_CONNECTWITH: 'metamask_connectWith', + /** Open MetaMask interface */ + METAMASK_OPEN: 'metamask_open', + /** Execute multiple operations in a batch */ + METAMASK_BATCH: 'metamask_batch', + /** Sign a message with personal_sign */ + PERSONAL_SIGN: 'personal_sign', + /** Request wallet permissions */ + WALLET_REQUESTPERMISSIONS: 'wallet_requestPermissions', + /** Revoke wallet permissions */ + WALLET_REVOKEPERMISSIONS: 'wallet_revokePermissions', + /** Get current wallet permissions */ + WALLET_GETPERMISSIONS: 'wallet_getPermissions', + /** Watch/add a token to the wallet */ + WALLET_WATCHASSET: 'wallet_watchAsset', + /** Add a new Ethereum chain */ + WALLET_ADDETHEREUMCHAIN: 'wallet_addEthereumChain', + /** Switch to a different Ethereum chain */ + WALLET_SWITCHETHEREUMCHAIN: 'wallet_switchEthereumChain', + /** Request account access */ + ETH_REQUESTACCOUNTS: 'eth_requestAccounts', + /** Get available accounts */ + ETH_ACCOUNTS: 'eth_accounts', + /** Get current chain ID */ + ETH_CHAINID: 'eth_chainId', + /** Send a transaction */ + ETH_SENDTRANSACTION: 'eth_sendTransaction', + /** Sign typed data */ + ETH_SIGNTYPEDDATA: 'eth_signTypedData', + /** Sign typed data v3 */ + ETH_SIGNTYPEDDATA_V3: 'eth_signTypedData_v3', + /** Sign typed data v4 */ + ETH_SIGNTYPEDDATA_V4: 'eth_signTypedData_v4', + /** Sign a transaction */ + ETH_SIGNTRANSACTION: 'eth_signTransaction', + /** Sign arbitrary data */ + ETH_SIGN: 'eth_sign', + /** Recover address from signature */ + PERSONAL_EC_RECOVER: 'personal_ecRecover', +}; + +/** + * Configuration mapping for RPC methods that should be redirected to the wallet. + * + * This constant defines which RPC methods require user interaction through + * the wallet interface (true) versus those that can be handled locally (false). + * Methods marked as true will redirect to the wallet for user approval. + */ +export const METHODS_TO_REDIRECT: { [method: string]: boolean } = { + [RPC_METHODS.ETH_REQUESTACCOUNTS]: true, + [RPC_METHODS.ETH_SENDTRANSACTION]: true, + [RPC_METHODS.ETH_SIGNTRANSACTION]: true, + [RPC_METHODS.ETH_SIGN]: true, + [RPC_METHODS.PERSONAL_SIGN]: true, + // stop redirecting these as we are caching values in the provider + [RPC_METHODS.ETH_ACCOUNTS]: false, + [RPC_METHODS.ETH_CHAINID]: false, + // + [RPC_METHODS.ETH_SIGNTYPEDDATA]: true, + [RPC_METHODS.ETH_SIGNTYPEDDATA_V3]: true, + [RPC_METHODS.ETH_SIGNTYPEDDATA_V4]: true, + [RPC_METHODS.WALLET_REQUESTPERMISSIONS]: true, + [RPC_METHODS.WALLET_GETPERMISSIONS]: true, + [RPC_METHODS.WALLET_WATCHASSET]: true, + [RPC_METHODS.WALLET_ADDETHEREUMCHAIN]: true, + [RPC_METHODS.WALLET_SWITCHETHEREUMCHAIN]: true, + [RPC_METHODS.METAMASK_CONNECTSIGN]: true, + [RPC_METHODS.METAMASK_CONNECTWITH]: true, + [RPC_METHODS.PERSONAL_EC_RECOVER]: true, + [RPC_METHODS.METAMASK_BATCH]: true, + [RPC_METHODS.METAMASK_OPEN]: true, +}; diff --git a/packages/sdk-multichain/src/domain/multichain/api/eip155.ts b/packages/sdk-multichain/src/domain/multichain/api/eip155.ts new file mode 100644 index 000000000..7d28c0dee --- /dev/null +++ b/packages/sdk-multichain/src/domain/multichain/api/eip155.ts @@ -0,0 +1,47 @@ +import type { RpcMethod } from './types'; + +//TODO: We probably want to avoid having to declare this and use the types from somewhere else +type EIP155 = { + methods: { + personal_sign: RpcMethod<{ message: string; account: string }, string>; + eth_accounts: RpcMethod; + eth_chainId: RpcMethod; + eth_sendTransaction: RpcMethod< + { to: string; value?: string; data?: string }, + string + >; + eth_call: RpcMethod<{ to: string; data?: string }, string>; + eth_getBalance: RpcMethod<{ address: string; blockNumber: string }, string>; + wallet_switchEthereumChain: RpcMethod<{ chainId: string }, void>; + wallet_addEthereumChain: RpcMethod< + { + chainId: string; + chainName: string; + nativeCurrency?: + | { + name: string; + symbol: string; + decimals: number; + } + | undefined; + rpcUrls: readonly string[]; + blockExplorerUrls?: string[] | undefined; + iconUrls?: string[] | undefined; + }, + void + >; + signAndSendTransaction: RpcMethod< + { to: string; value?: string; data?: string }, + string + >; + signTransaction: RpcMethod< + { to: string; value?: string; data?: string }, + string + >; + signMessage: RpcMethod<{ message: string }, string>; + signIn: RpcMethod<{ message: string }, string>; + }; + events: ['eth_subscription']; +}; + +export default EIP155; diff --git a/packages/sdk-multichain/src/domain/multichain/api/types.ts b/packages/sdk-multichain/src/domain/multichain/api/types.ts new file mode 100644 index 000000000..b0383db79 --- /dev/null +++ b/packages/sdk-multichain/src/domain/multichain/api/types.ts @@ -0,0 +1,100 @@ +import { Json } from "@metamask/utils"; +import type EIP155 from "./eip155"; + +/** + * Represents a blockchain scope identifier in CAIP format. + * + * Scopes define which blockchain networks and standards the SDK + * can interact with. The format follows CAIP standards for + * blockchain identification. + * + * @template T - The RPC API type to extract available scopes from + */ +export type Scope = + | `eip155:${string}` + | `solana:${string}` + | `${Extract}:${string}`; + +/** + * Represents a generic RPC (Remote Procedure Call) method function type. + * + * This type defines the signature for RPC methods that can be either synchronous + * or asynchronous, providing flexibility for different types of API calls. + * + * @template Params - The type of parameters that the RPC method accepts + * @template Return - The type of value that the RPC method returns + * + * @param params - The parameters to pass to the RPC method + * @returns Either a Promise that resolves to the return value, or the return value directly + */ +export type RpcMethod = (params: Params) => Promise | Return; + +/** + * Defines the structure of the RPC API interface. + * + * This type represents the available RPC APIs organized by blockchain standard. + * Currently supports EIP-155 (Ethereum) with the potential for additional + * blockchain standards to be added in the future. + */ +export type RPCAPI = { + /** EIP-155 compliant RPC methods for Ethereum-based chains */ + eip155: EIP155; +}; + +/** + * Represents a notification object for RPC communication. + * + * Notifications are one-way messages sent from the server to the client + * that don't require a response. They follow the JSON-RPC 2.0 specification + * for notification messages. + */ +export type Notification = { + /** The name of the method being called */ + method: string; + /** Optional parameters for the method call */ + params?: Json; + /** JSON-RPC version identifier (typically "2.0") */ + jsonrpc?: string; +}; + +/** + * Callback function type for handling incoming notifications. + * + * This type defines the signature for functions that process notification + * messages received from RPC connections. + * + * @param notification - The notification object to handle + */ +export type NotificationCallback = (notification: Notification) => void; + +/** + * Options for invoking RPC methods with specific scope and request parameters. + * + * This type defines the structure for method invocation options, allowing + * callers to specify both the blockchain scope and the specific request details. + */ +export type InvokeMethodOptions = { + /** The blockchain scope/standard to use for the method call */ + scope: Scope; + /** The request details including method name and parameters */ + request: { + /** The name of the RPC method to invoke */ + method: string; + /** The parameters to pass to the RPC method */ + params: unknown; + }; +}; + + +/** + * Mapping of CAIP chain IDs to their corresponding RPC URLs. + * + * This type defines the structure for providing custom RPC endpoints + * for different blockchain networks using CAIP-2 format identifiers. + */ +export type RPC_URLS_MAP = { + /** CAIP-2 format chain ID mapped to its RPC URL (e.g., "eip155:1" -> "https://...") */ + [chainId: `${string}:${string}`]: string; +}; + + diff --git a/packages/sdk-multichain/src/domain/multichain/index.ts b/packages/sdk-multichain/src/domain/multichain/index.ts new file mode 100644 index 000000000..7b4481f8a --- /dev/null +++ b/packages/sdk-multichain/src/domain/multichain/index.ts @@ -0,0 +1,145 @@ +import type { SessionData } from '@metamask/multichain-api-client'; +import type { CaipAccountId, Json } from '@metamask/utils'; + +import type { StoreClient } from '../store/client'; +import type { InvokeMethodOptions, NotificationCallback, RPC_URLS_MAP, Scope } from './api/types'; + +/** + * Configuration settings for the dapp using the SDK. + * + * This type allows for two variants of dapp configuration: + * - Using a regular icon URL + * - Using a base64-encoded icon + */ +export type DappSettings = + | { name?: string; url?: string; iconUrl?: string } + | { name?: string; url?: string; base64Icon?: string }; + +/** + * Constructor options for creating a Multichain SDK instance. + * + * This type defines all the configuration options available when + * initializing the SDK, including dapp settings, API configuration, + * analytics, storage, UI preferences, and transport options. + */ +export type MultichainSDKConstructor = { + /** Dapp identification and branding settings */ + dapp: DappSettings; + /** Optional API configuration for external services */ + api?: { + /** The Infura API key to use for RPC requests */ + infuraAPIKey?: string; + /** A map of RPC URLs to use for read-only requests */ + readonlyRPCMap?: RPC_URLS_MAP; + }; + /** Analytics configuration */ + analytics: { enabled: false } | { enabled: true; integrationType: string }; + /** Storage client for persisting SDK data */ + storage: StoreClient; + /** UI configuration options */ + ui: { headless: boolean }; + /** Optional transport configuration */ + transport?: { + /** Extension ID for browser extension transport */ + extensionId?: string; + }; +}; + +/** + * Abstract base class for the Multichain SDK implementation. + * + * This class defines the core interface that all Multichain SDK implementations + * must provide, including session management, connection handling, and method invocation. + */ +/* c8 ignore start */ +export abstract class MultichainSDKBase { + /** + * Establishes a connection to the multichain provider. + * + * @returns Promise that resolves to true if connection is successful, false otherwise + */ + abstract connect(): Promise; + + /** + * Disconnects from the multichain provider. + * + * @returns Promise that resolves when disconnection is complete + */ + abstract disconnect(): Promise; + + /** + * Retrieves the current session data. + * + * @returns Promise that resolves to the current session data, or undefined if no session exists + */ + abstract getSession(): Promise; + + /** + * Creates a new session with the specified scopes and account IDs. + * + * @param scopes - Array of blockchain scopes to request access to + * @param caipAccountIds - Array of CAIP account IDs to associate with the session + * @returns Promise that resolves to the created session data + */ + abstract createSession( + scopes: Scope[], + caipAccountIds: CaipAccountId[], + ): Promise; + + /** + * Revokes the current session. + * + * @returns Promise that resolves when the session has been revoked + */ + abstract revokeSession(): Promise; + + /** + * Registers a listener for incoming notifications. + * + * @param listener - Callback function to handle notifications + * @returns Function to remove the listener + */ + abstract onNotification(listener: NotificationCallback): () => void; + + /** + * Invokes an RPC method with the specified options. + * + * @param options - The method invocation options including scope and request details + * @returns Promise that resolves to the method result + */ + abstract invokeMethod(options: InvokeMethodOptions): Promise; + + /** + * Storage client instance for persisting SDK data. + */ + abstract readonly storage: StoreClient; +} +/* c8 ignore end */ + +export type { SessionData } from '@metamask/multichain-api-client'; + +/** + * Base options for Multichain SDK configuration. + * + * This type includes the core configuration options excluding storage, + * which is handled separately in the full SDK options. + */ +export type MultichainSDKBaseOptions = Pick< + MultichainSDKConstructor, + 'dapp' | 'analytics' | 'ui' | 'transport' +>; + +/** + * Complete options for Multichain SDK configuration. + * + * This type extends the base options with storage configuration, + * providing all necessary options for SDK initialization. + */ +export type MultichainSDKOptions = MultichainSDKBaseOptions & { + /** Storage client for persisting SDK data */ + storage: StoreClient; +}; + +export type CreateMultichainFN = ( options: MultichainSDKBaseOptions ) => Promise; + +export type * from './api/types'; diff --git a/packages/sdk-multichain/src/domain/platform/index.ts b/packages/sdk-multichain/src/domain/platform/index.ts new file mode 100644 index 000000000..c21c643c3 --- /dev/null +++ b/packages/sdk-multichain/src/domain/platform/index.ts @@ -0,0 +1,75 @@ +import Bowser from 'bowser'; + +export enum PlatformType { + // React Native or Nodejs + NonBrowser = 'nodejs', + // MetaMask Mobile in-app browser + MetaMaskMobileWebview = 'in-app-browser', + // Desktop Browser + DesktopWeb = 'web-desktop', + // Mobile Browser + MobileWeb = 'web-mobile', + // ReactNative + ReactNative = 'react-native', +} + +function isNotBrowser() { + if (typeof window === 'undefined') { + return true; + } + if (!window?.navigator) { + return true + } + if (typeof global !== 'undefined' && + global?.navigator?.product === 'ReactNative') { + return true; + } + return navigator?.product === 'ReactNative' +} + +function isReactNative() { + const hasWindowNavigator = typeof window !== 'undefined' && window.navigator !== undefined; + const navigator = hasWindowNavigator ? + window.navigator : + undefined; + + if (!navigator) { + return false; + } + + return hasWindowNavigator && window.navigator?.product === 'ReactNative' +} + +function isMetaMaskMobileWebView() { + return ( + typeof window !== 'undefined' && + Boolean(window.ReactNativeWebView) && + Boolean(window.navigator.userAgent.endsWith('MetaMaskMobile')) + ); +} + +function isMobile() { + const browser = Bowser.parse(window.navigator.userAgent); + return ( + browser?.platform?.type === 'mobile' || browser?.platform?.type === 'tablet' + ); +} + +/** + * + */ +export function getPlatformType() { + if (isReactNative()) { + return PlatformType.ReactNative; + } + if (isNotBrowser()) { + return PlatformType.NonBrowser; + } + if (isMetaMaskMobileWebView()) { + return PlatformType.MetaMaskMobileWebview; + } + if (isMobile()) { + return PlatformType.MobileWeb; + } + return PlatformType.DesktopWeb; +} diff --git a/packages/sdk-multichain/src/domain/store/adapter.ts b/packages/sdk-multichain/src/domain/store/adapter.ts new file mode 100644 index 000000000..304a8602b --- /dev/null +++ b/packages/sdk-multichain/src/domain/store/adapter.ts @@ -0,0 +1,12 @@ +/* c8 ignore start */ +export type StoreOptions = Record; + +export abstract class StoreAdapter { + constructor(public options?: StoreOptions) {} + + abstract getItem(key: string): Promise; + + abstract setItem(key: string, value: string): Promise; + + abstract deleteItem(key: string): Promise; +} diff --git a/packages/sdk-multichain/src/domain/store/client.ts b/packages/sdk-multichain/src/domain/store/client.ts new file mode 100644 index 000000000..6575ef7fa --- /dev/null +++ b/packages/sdk-multichain/src/domain/store/client.ts @@ -0,0 +1,34 @@ +/* c8 ignore start */ +export interface ChannelConfig { + channelId: string; + validUntil: number; + otherKey?: string; + localKey?: string; + walletVersion?: string; + deeplinkProtocolAvailable?: boolean; + relayPersistence?: boolean; // Set if the session has full relay persistence (can exchange message without the other side connected) + /** + * lastActive: ms value of the last time connection was ready CLIENTS_READY event. + * */ + lastActive?: number; +} + +export abstract class StoreClient { + abstract getAnonId(): Promise; + + abstract getExtensionId(): Promise; + + abstract getChannelConfig(): Promise; + + abstract setExtensionId(extensionId: string): Promise; + + abstract setAnonId(anonId: string): Promise; + + abstract setChannelConfig(channelConfig: ChannelConfig): Promise; + + abstract removeExtensionId(): Promise; + + abstract removeAnonId(): Promise; + + abstract getDebug(): Promise; +} diff --git a/packages/sdk-multichain/src/domain/store/index.ts b/packages/sdk-multichain/src/domain/store/index.ts new file mode 100644 index 000000000..61bceef04 --- /dev/null +++ b/packages/sdk-multichain/src/domain/store/index.ts @@ -0,0 +1,3 @@ + +export * from './adapter'; +export * from './client'; diff --git a/packages/sdk-multichain/src/globals.d.ts b/packages/sdk-multichain/src/globals.d.ts new file mode 100644 index 000000000..e83dab810 --- /dev/null +++ b/packages/sdk-multichain/src/globals.d.ts @@ -0,0 +1,11 @@ +declare module '@paulmillr/qr'; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export declare const mmsdk: any; +declare global { + interface Window { + /** + * TODO: Add types for the window object to manage connection with inApp browser, etc + */ + ReactNativeWebView?: any; + } +} diff --git a/packages/sdk-multichain/src/index.browser.ts b/packages/sdk-multichain/src/index.browser.ts new file mode 100644 index 000000000..00523e58c --- /dev/null +++ b/packages/sdk-multichain/src/index.browser.ts @@ -0,0 +1,2 @@ +export type * from './domain'; + diff --git a/packages/sdk-multichain/src/index.native.ts b/packages/sdk-multichain/src/index.native.ts new file mode 100644 index 000000000..00523e58c --- /dev/null +++ b/packages/sdk-multichain/src/index.native.ts @@ -0,0 +1,2 @@ +export type * from './domain'; + diff --git a/packages/sdk-multichain/src/index.node.ts b/packages/sdk-multichain/src/index.node.ts new file mode 100644 index 000000000..00523e58c --- /dev/null +++ b/packages/sdk-multichain/src/index.node.ts @@ -0,0 +1,2 @@ +export type * from './domain'; + diff --git a/packages/sdk-multichain/tsconfig.json b/packages/sdk-multichain/tsconfig.json new file mode 100644 index 000000000..09ffe9b0c --- /dev/null +++ b/packages/sdk-multichain/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "baseUrl": "./", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "strict": true, + "outDir": "dist", + "target": "es6", + "esModuleInterop": true, + "module": "ES2020", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "incremental": false, + "lib": ["DOM", "ES2016"], + "skipLibCheck": true, + "types": ["node"], + "paths": { + //"@metamask/sdk-install-modal-web/*": ["../sdk-install-modal-web/dist/*"] + } + // "typeRoots": ["packages/sdk/src/types/global.d.ts", "src/types/global.d.ts"] + }, + "include": ["./src", "./tools"], + "exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"] +} diff --git a/packages/sdk-multichain/tsup.config.ts b/packages/sdk-multichain/tsup.config.ts new file mode 100644 index 000000000..ffdaf612f --- /dev/null +++ b/packages/sdk-multichain/tsup.config.ts @@ -0,0 +1,165 @@ +import { defineConfig, type Options } from 'tsup'; +import { umdWrapper } from 'esbuild-plugin-umd-wrapper'; + +const packageJson = require('./package.json'); + +// Dependencies categorization (same as rollup config) +const peerDependencies = Object.keys(packageJson.peerDependencies || {}); +const optionalDependencies = Object.keys(packageJson.optionalDependencies || {}); + +// Dependencies that should be bundled +const bundledDeps = [ 'readable-stream']; +// Shared dependencies that should be deduplicated +const sharedDeps = ['eventemitter2', 'socket.io-client', 'debug', 'uuid', 'cross-fetch', '@metamask/sdk-analytics']; + +// Filter function to exclude bundled dependencies +const excludeBundledDeps = (dep: string) => !bundledDeps.includes(dep); + +// Dependencies that should always be external +const baseExternalDeps = [ + ...peerDependencies.filter(excludeBundledDeps), + ...optionalDependencies.filter(excludeBundledDeps), + ...sharedDeps, + '@react-native-async-storage/async-storage', + 'extension-port-stream', + '@metamask/utils', +]; + +// Platform-specific externals +const webExternalDeps = [...baseExternalDeps].filter(excludeBundledDeps); +const rnExternalDeps = [...baseExternalDeps].filter(excludeBundledDeps); +const nodeExternalDeps = [...baseExternalDeps].filter(excludeBundledDeps); + +// Base configuration shared across all builds +const baseConfig: Partial = { + bundle: true, + tsconfig: './tsconfig.json', + sourcemap: true, + metafile: true, + clean: false, // We handle cleaning via scripts + dts: true, // We handle types separately via tsc + splitting: false, // Keep bundle as single file to match rollup, +}; + + +const entryName = packageJson.name.replace('@metamask/', ''); + +// TSUP Configuration +export default defineConfig([ + { + ...baseConfig, + entry: { [entryName]: 'src/index.browser.ts' }, + outDir: 'dist/browser/es', + format: 'esm', + platform: 'browser', + external: webExternalDeps, + esbuildOptions: (options) => { + options.metafile = true; + options.platform = 'browser'; + options.mainFields = ['browser', 'module', 'main']; + options.conditions = ['browser']; + options.outExtension = { ".js": '.mjs' }; + }, + banner: { + js: '/* Browser ES build */', + } + }, + { + ...baseConfig, + entry: { [entryName]: 'src/index.browser.ts' }, + outDir: 'dist/browser/umd', + platform: 'browser', + external: [...baseExternalDeps, ...peerDependencies], + esbuildPlugins: [ + umdWrapper({}) as any, + ], + esbuildOptions: (options) => { + options.metafile = true; + options.outExtension = { ".js": '.js' }; + options.platform = 'browser'; + options.mainFields = ['browser', 'module', 'main']; + options.conditions = ['browser']; + }, + banner: { + js: '/* Browser UMD build */', + }, + }, + { + ...baseConfig, + entry: { [entryName]: 'src/index.browser.ts' }, + outDir: 'dist/browser/iife', + format: 'iife', + platform: 'browser', + external: [...baseExternalDeps, ...peerDependencies], + globalName: 'MetaMaskSDK', // Matches rollup IIFE config + esbuildOptions: (options) => { + options.metafile = true; + options.outExtension = { ".js": '.js' }; + options.platform = 'browser'; + options.mainFields = ['browser', 'module', 'main']; + options.conditions = ['browser']; + }, + banner: { + js: '/* Browser IIFE build */', + }, + }, + { + ...baseConfig, + entry: { [entryName]: 'src/index.node.ts' }, + outDir: 'dist/node/cjs', + format: 'cjs', + platform: 'node', + external: nodeExternalDeps, + esbuildOptions: (options) => { + options.metafile = true; + options.platform = 'node'; + options.mainFields = ['module', 'main']; + options.conditions = ['node']; + options.outExtension = { ".js": '.js' }; + }, + banner: { + js: '/* Node.js CJS build */', + }, + }, + { + ...baseConfig, + entry: { [entryName]: 'src/index.node.ts' }, + outDir: 'dist/node/es', + format: 'esm', + platform: 'node', + external: nodeExternalDeps, + esbuildOptions: (options) => { + options.metafile = true; + options.platform = 'node'; + options.mainFields = ['module', 'main']; + options.conditions = ['node']; + options.outExtension = { ".js": '.mjs' }; + }, + banner: { + js: '/* Node.js ES build */', + }, + }, + { + ...baseConfig, + entry: { [entryName]: 'src/index.native.ts' }, + outDir: 'dist/react-native/es', + format: 'esm', + platform: 'neutral', // React Native is neither pure browser nor pure node + external: rnExternalDeps, + esbuildOptions: (options) => { + options.metafile = true; + options.mainFields = ['react-native', 'node', 'browser']; + options.conditions = ['react-native', 'node', 'browser']; + options.outExtension = { ".js": '.mjs' }; + }, + banner: { + js: '/* React Native ES build */', + }, + }, + { + ...baseConfig, + entry: { [entryName]: 'src/index.browser.ts' }, + outDir: 'dist/types', + dts: { only: true }, + } +]); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index d9dbb70c6..b079b25e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -69,6 +69,16 @@ __metadata: languageName: node linkType: hard +"@ampproject/remapping@npm:^2.3.0": + version: 2.3.0 + resolution: "@ampproject/remapping@npm:2.3.0" + dependencies: + "@jridgewell/gen-mapping": ^0.3.5 + "@jridgewell/trace-mapping": ^0.3.24 + checksum: d3ad7b89d973df059c4e8e6d7c972cbeb1bb2f18f002a3bd04ae0707da214cb06cc06929b65aa2313b9347463df2914772298bae8b1d7973f246bb3f2ab3e8f0 + languageName: node + linkType: hard + "@apideck/better-ajv-errors@npm:^0.3.1": version: 0.3.6 resolution: "@apideck/better-ajv-errors@npm:0.3.6" @@ -1362,6 +1372,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/helper-string-parser@npm:7.27.1" + checksum: 0a8464adc4b39b138aedcb443b09f4005d86207d7126e5e079177e05c3116107d856ec08282b365e9a79a9872f40f4092a6127f8d74c8a01c1ef789dacfc25d6 + languageName: node + linkType: hard + "@babel/helper-validator-identifier@npm:^7.22.15": version: 7.22.15 resolution: "@babel/helper-validator-identifier@npm:7.22.15" @@ -1404,6 +1421,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-identifier@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/helper-validator-identifier@npm:7.27.1" + checksum: 3c7e8391e59d6c85baeefe9afb86432f2ab821c6232b00ea9082a51d3e7e95a2f3fb083d74dc1f49ac82cf238e1d2295dafcb001f7b0fab479f3f56af5eaaa47 + languageName: node + linkType: hard + "@babel/helper-validator-option@npm:^7.22.15": version: 7.22.15 resolution: "@babel/helper-validator-option@npm:7.22.15" @@ -1664,6 +1688,17 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.25.4": + version: 7.28.0 + resolution: "@babel/parser@npm:7.28.0" + dependencies: + "@babel/types": ^7.28.0 + bin: + parser: ./bin/babel-parser.js + checksum: 718e4ce9b0914701d6f74af610d3e7d52b355ef1dcf34a7dedc5930e96579e387f04f96187e308e601828b900b8e4e66d2fe85023beba2ac46587023c45b01cf + languageName: node + linkType: hard + "@babel/parser@npm:^7.25.9, @babel/parser@npm:^7.26.2": version: 7.26.2 resolution: "@babel/parser@npm:7.26.2" @@ -6045,6 +6080,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.25.4, @babel/types@npm:^7.28.0": + version: 7.28.0 + resolution: "@babel/types@npm:7.28.0" + dependencies: + "@babel/helper-string-parser": ^7.27.1 + "@babel/helper-validator-identifier": ^7.27.1 + checksum: 3cb33bbe79e9629c3e4ed1592340f936481e7aef2c3df11f8b1f91e54b45e89b3ad92f2d20f8acdb5a7e00157174ffe8b1d174069bb839303e7f39f579d60969 + languageName: node + linkType: hard + "@babel/types@npm:^7.25.9, @babel/types@npm:^7.26.0": version: 7.26.0 resolution: "@babel/types@npm:7.26.0" @@ -6069,6 +6114,104 @@ __metadata: languageName: node linkType: hard +"@bcoe/v8-coverage@npm:^1.0.2": + version: 1.0.2 + resolution: "@bcoe/v8-coverage@npm:1.0.2" + checksum: f4e6f55817645fc1b543aa0bbd6ffceb7b9ff3052e8c92c493a0a71831e6b8ae97d73e123b048cb98ef9d9e31afae018a60795f2e27a6f3e94a1ec7abedac85d + languageName: node + linkType: hard + +"@biomejs/biome@npm:2.0.0": + version: 2.0.0 + resolution: "@biomejs/biome@npm:2.0.0" + dependencies: + "@biomejs/cli-darwin-arm64": 2.0.0 + "@biomejs/cli-darwin-x64": 2.0.0 + "@biomejs/cli-linux-arm64": 2.0.0 + "@biomejs/cli-linux-arm64-musl": 2.0.0 + "@biomejs/cli-linux-x64": 2.0.0 + "@biomejs/cli-linux-x64-musl": 2.0.0 + "@biomejs/cli-win32-arm64": 2.0.0 + "@biomejs/cli-win32-x64": 2.0.0 + dependenciesMeta: + "@biomejs/cli-darwin-arm64": + optional: true + "@biomejs/cli-darwin-x64": + optional: true + "@biomejs/cli-linux-arm64": + optional: true + "@biomejs/cli-linux-arm64-musl": + optional: true + "@biomejs/cli-linux-x64": + optional: true + "@biomejs/cli-linux-x64-musl": + optional: true + "@biomejs/cli-win32-arm64": + optional: true + "@biomejs/cli-win32-x64": + optional: true + bin: + biome: bin/biome + checksum: 2c2c8ff6e1f48b2e676017315ba6ccc829d99a8ffcf631cd263d830e5e820ba85b1f75894b4a505b93b8c71394cbf91c310c25da6ae6778920acbbfd744dd507 + languageName: node + linkType: hard + +"@biomejs/cli-darwin-arm64@npm:2.0.0": + version: 2.0.0 + resolution: "@biomejs/cli-darwin-arm64@npm:2.0.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@biomejs/cli-darwin-x64@npm:2.0.0": + version: 2.0.0 + resolution: "@biomejs/cli-darwin-x64@npm:2.0.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@biomejs/cli-linux-arm64-musl@npm:2.0.0": + version: 2.0.0 + resolution: "@biomejs/cli-linux-arm64-musl@npm:2.0.0" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@biomejs/cli-linux-arm64@npm:2.0.0": + version: 2.0.0 + resolution: "@biomejs/cli-linux-arm64@npm:2.0.0" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@biomejs/cli-linux-x64-musl@npm:2.0.0": + version: 2.0.0 + resolution: "@biomejs/cli-linux-x64-musl@npm:2.0.0" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@biomejs/cli-linux-x64@npm:2.0.0": + version: 2.0.0 + resolution: "@biomejs/cli-linux-x64@npm:2.0.0" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@biomejs/cli-win32-arm64@npm:2.0.0": + version: 2.0.0 + resolution: "@biomejs/cli-win32-arm64@npm:2.0.0" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@biomejs/cli-win32-x64@npm:2.0.0": + version: 2.0.0 + resolution: "@biomejs/cli-win32-x64@npm:2.0.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@callstack/react-theme-provider@npm:^3.0.9": version: 3.0.9 resolution: "@callstack/react-theme-provider@npm:3.0.9" @@ -10476,6 +10619,16 @@ __metadata: languageName: node linkType: hard +"@jridgewell/trace-mapping@npm:^0.3.23": + version: 0.3.29 + resolution: "@jridgewell/trace-mapping@npm:0.3.29" + dependencies: + "@jridgewell/resolve-uri": ^3.1.0 + "@jridgewell/sourcemap-codec": ^1.4.14 + checksum: 5e92eeafa5131a4f6b7122063833d657f885cb581c812da54f705d7a599ff36a75a4a093a83b0f6c7e95642f5772dd94753f696915e8afea082237abf7423ca3 + languageName: node + linkType: hard + "@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": version: 0.3.25 resolution: "@jridgewell/trace-mapping@npm:0.3.25" @@ -10994,6 +11147,39 @@ __metadata: languageName: node linkType: hard +"@metamask/multichain-api-client@npm:^0.6.4": + version: 0.6.4 + resolution: "@metamask/multichain-api-client@npm:0.6.4" + checksum: 6368445238f4cc87fd82721039f4e6f3401c57a0f03691a313457f43c9efc521b33b918029a1eec3743274a45fc112cbd725aa893744c6fecb0fa857eae58163 + languageName: node + linkType: hard + +"@metamask/multichain-sdk@workspace:packages/sdk-multichain": + version: 0.0.0-use.local + resolution: "@metamask/multichain-sdk@workspace:packages/sdk-multichain" + dependencies: + "@biomejs/biome": 2.0.0 + "@metamask/auto-changelog": ^3.4.3 + "@metamask/multichain-api-client": ^0.6.4 + "@metamask/sdk-analytics": "workspace:^" + "@metamask/utils": ^11.4.0 + "@paulmillr/qr": ^0.2.1 + "@react-native-async-storage/async-storage": ^1.19.6 + "@vitest/coverage-v8": ^3.2.4 + bowser: ^2.11.0 + cross-fetch: ^4.1.0 + esbuild-plugin-umd-wrapper: ^3.0.0 + eventemitter2: ^6.4.9 + nock: ^14.0.4 + prettier: ^3.3.3 + tsup: ^8.5.0 + typescript: ^5.8.3 + typescript-eslint: ^8.6.0 + uuid: ^11.1.0 + vitest: ^3.1.2 + languageName: unknown + linkType: soft + "@metamask/network-controller@npm:^16.0.0": version: 16.0.0 resolution: "@metamask/network-controller@npm:16.0.0" @@ -11137,7 +11323,7 @@ __metadata: languageName: node linkType: hard -"@metamask/sdk-analytics@workspace:*, @metamask/sdk-analytics@workspace:packages/sdk-analytics": +"@metamask/sdk-analytics@workspace:*, @metamask/sdk-analytics@workspace:^, @metamask/sdk-analytics@workspace:packages/sdk-analytics": version: 0.0.0-use.local resolution: "@metamask/sdk-analytics@workspace:packages/sdk-analytics" dependencies: @@ -11857,6 +12043,13 @@ __metadata: languageName: unknown linkType: soft +"@metamask/superstruct@npm:^3.1.0": + version: 3.2.1 + resolution: "@metamask/superstruct@npm:3.2.1" + checksum: 194e4afc4df89f347e4dd16db8f8dfcbf7990ff82169c3bd43b98ecff2f1ef09488b987af612cc1ea2689826e8460bb2b01e1a3a340383420115b3a90aa68465 + languageName: node + linkType: hard + "@metamask/swappable-obj-proxy@npm:^2.1.0": version: 2.1.0 resolution: "@metamask/swappable-obj-proxy@npm:2.1.0" @@ -11864,6 +12057,24 @@ __metadata: languageName: node linkType: hard +"@metamask/utils@npm:^11.4.0": + version: 11.4.2 + resolution: "@metamask/utils@npm:11.4.2" + dependencies: + "@ethereumjs/tx": ^4.2.0 + "@metamask/superstruct": ^3.1.0 + "@noble/hashes": ^1.3.1 + "@scure/base": ^1.1.3 + "@types/debug": ^4.1.7 + debug: ^4.3.4 + lodash.memoize: ^4.1.2 + pony-cause: ^2.1.10 + semver: ^7.5.4 + uuid: ^9.0.1 + checksum: 11061a93f49684563a14caaaab2d8dbb969c907dbc24358cf188dd10ec00ac91e5d04369ef605e9d78e75f8ad53d9a0fbdb65f2325b12ef6c8db85bb46160dff + languageName: node + linkType: hard + "@metamask/utils@npm:^3.0.1": version: 3.6.0 resolution: "@metamask/utils@npm:3.6.0" @@ -20814,6 +21025,33 @@ __metadata: languageName: node linkType: hard +"@vitest/coverage-v8@npm:^3.2.4": + version: 3.2.4 + resolution: "@vitest/coverage-v8@npm:3.2.4" + dependencies: + "@ampproject/remapping": ^2.3.0 + "@bcoe/v8-coverage": ^1.0.2 + ast-v8-to-istanbul: ^0.3.3 + debug: ^4.4.1 + istanbul-lib-coverage: ^3.2.2 + istanbul-lib-report: ^3.0.1 + istanbul-lib-source-maps: ^5.0.6 + istanbul-reports: ^3.1.7 + magic-string: ^0.30.17 + magicast: ^0.3.5 + std-env: ^3.9.0 + test-exclude: ^7.0.1 + tinyrainbow: ^2.0.0 + peerDependencies: + "@vitest/browser": 3.2.4 + vitest: 3.2.4 + peerDependenciesMeta: + "@vitest/browser": + optional: true + checksum: b33d4abb32216c793b1da122254bf70578c4acddbf2011c377818cbfc9506383398a5a2eaeb70045120dac1f86a1a2511fd624050dd92ef7387b9469929ceb33 + languageName: node + linkType: hard + "@vitest/expect@npm:3.1.2": version: 3.1.2 resolution: "@vitest/expect@npm:3.1.2" @@ -23192,6 +23430,17 @@ __metadata: languageName: node linkType: hard +"ast-v8-to-istanbul@npm:^0.3.3": + version: 0.3.3 + resolution: "ast-v8-to-istanbul@npm:0.3.3" + dependencies: + "@jridgewell/trace-mapping": ^0.3.25 + estree-walker: ^3.0.3 + js-tokens: ^9.0.1 + checksum: 749bde3ec1c273797f8c6d3e5c0bcf4a29c21e2eecdc4302de980ddd4bac6bc94a3c64159e2b37e95b756203f101183905f0ead7567d40e5a4d739ac3923b94c + languageName: node + linkType: hard + "astral-regex@npm:^1.0.0": version: 1.0.0 resolution: "astral-regex@npm:1.0.0" @@ -25954,6 +26203,13 @@ __metadata: languageName: node linkType: hard +"confbox@npm:^0.1.8": + version: 0.1.8 + resolution: "confbox@npm:0.1.8" + checksum: 5c7718ab22cf9e35a31c21ef124156076ae8c9dc65e6463d54961caf5a1d529284485a0fdf83fd23b27329f3b75b0c8c07d2e36c699f5151a2efe903343f976a + languageName: node + linkType: hard + "confusing-browser-globals@npm:^1.0.11": version: 1.0.11 resolution: "confusing-browser-globals@npm:1.0.11" @@ -26433,6 +26689,15 @@ __metadata: languageName: node linkType: hard +"cross-fetch@npm:^4.1.0": + version: 4.1.0 + resolution: "cross-fetch@npm:4.1.0" + dependencies: + node-fetch: ^2.7.0 + checksum: c02fa85d59f83e50dbd769ee472c9cc984060c403ee5ec8654659f61a525c1a655eef1c7a35e365c1a107b4e72d76e786718b673d1cb3c97f61d4644cb0a9f9d + languageName: node + linkType: hard + "cross-spawn@npm:^6.0.0, cross-spawn@npm:^6.0.5": version: 6.0.6 resolution: "cross-spawn@npm:6.0.6" @@ -27074,6 +27339,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:^4.4.1": + version: 4.4.1 + resolution: "debug@npm:4.4.1" + dependencies: + ms: ^2.1.3 + peerDependenciesMeta: + supports-color: + optional: true + checksum: a43826a01cda685ee4cec00fb2d3322eaa90ccadbef60d9287debc2a886be3e835d9199c80070ede75a409ee57828c4c6cd80e4b154f2843f0dc95a570dc0729 + languageName: node + linkType: hard + "decamelize-keys@npm:^1.1.0": version: 1.1.1 resolution: "decamelize-keys@npm:1.1.1" @@ -29144,6 +29421,13 @@ __metadata: languageName: node linkType: hard +"esbuild-plugin-umd-wrapper@npm:^3.0.0": + version: 3.0.0 + resolution: "esbuild-plugin-umd-wrapper@npm:3.0.0" + checksum: 2358baee040a99c964b39980192d952ec50d61780726193e336fe8e81df49622fac3b371e3fea26e730c9293ca7f04ecd391160b31e468b53e8fb854b6cb985e + languageName: node + linkType: hard + "esbuild-register@npm:^3.5.0": version: 3.5.0 resolution: "esbuild-register@npm:3.5.0" @@ -32093,6 +32377,17 @@ __metadata: languageName: node linkType: hard +"fix-dts-default-cjs-exports@npm:^1.0.0": + version: 1.0.1 + resolution: "fix-dts-default-cjs-exports@npm:1.0.1" + dependencies: + magic-string: ^0.30.17 + mlly: ^1.7.4 + rollup: ^4.34.8 + checksum: 3324418bb63c93b6b22a808e242d220caba804860c24218b2912abc4525525334fcdcb62d22be6472a8d84ee2ad4165bc79554140c3369eb11d23220cdd986ce + languageName: node + linkType: hard + "flat-cache@npm:^3.0.4": version: 3.0.4 resolution: "flat-cache@npm:3.0.4" @@ -33000,7 +33295,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.3.10": +"glob@npm:^10.3.10, glob@npm:^10.4.1": version: 10.4.5 resolution: "glob@npm:10.4.5" dependencies: @@ -35398,6 +35693,13 @@ __metadata: languageName: node linkType: hard +"istanbul-lib-coverage@npm:^3.2.2": + version: 3.2.2 + resolution: "istanbul-lib-coverage@npm:3.2.2" + checksum: 2367407a8d13982d8f7a859a35e7f8dd5d8f75aae4bb5484ede3a9ea1b426dc245aff28b976a2af48ee759fdd9be374ce2bd2669b644f31e76c5f46a2e29a831 + languageName: node + linkType: hard + "istanbul-lib-instrument@npm:^5.0.4, istanbul-lib-instrument@npm:^5.1.0": version: 5.2.1 resolution: "istanbul-lib-instrument@npm:5.2.1" @@ -35435,6 +35737,17 @@ __metadata: languageName: node linkType: hard +"istanbul-lib-report@npm:^3.0.1": + version: 3.0.1 + resolution: "istanbul-lib-report@npm:3.0.1" + dependencies: + istanbul-lib-coverage: ^3.0.0 + make-dir: ^4.0.0 + supports-color: ^7.1.0 + checksum: fd17a1b879e7faf9bb1dc8f80b2a16e9f5b7b8498fe6ed580a618c34df0bfe53d2abd35bf8a0a00e628fb7405462576427c7df20bbe4148d19c14b431c974b21 + languageName: node + linkType: hard + "istanbul-lib-source-maps@npm:^4.0.0": version: 4.0.1 resolution: "istanbul-lib-source-maps@npm:4.0.1" @@ -35446,6 +35759,17 @@ __metadata: languageName: node linkType: hard +"istanbul-lib-source-maps@npm:^5.0.6": + version: 5.0.6 + resolution: "istanbul-lib-source-maps@npm:5.0.6" + dependencies: + "@jridgewell/trace-mapping": ^0.3.23 + debug: ^4.1.1 + istanbul-lib-coverage: ^3.0.0 + checksum: 8dd6f2c1e2ecaacabeef8dc9ab52c4ed0a6036310002cf7f46ea6f3a5fb041da8076f5350e6a6be4c60cd4f231c51c73e042044afaf44820d857d92ecfb8ab6c + languageName: node + linkType: hard + "istanbul-reports@npm:^3.1.3": version: 3.1.5 resolution: "istanbul-reports@npm:3.1.5" @@ -35466,6 +35790,16 @@ __metadata: languageName: node linkType: hard +"istanbul-reports@npm:^3.1.7": + version: 3.1.7 + resolution: "istanbul-reports@npm:3.1.7" + dependencies: + html-escaper: ^2.0.0 + istanbul-lib-report: ^3.0.0 + checksum: 2072db6e07bfbb4d0eb30e2700250636182398c1af811aea5032acb219d2080f7586923c09fa194029efd6b92361afb3dcbe1ebcc3ee6651d13340f7c6c4ed95 + languageName: node + linkType: hard + "iterate-iterator@npm:^1.0.1": version: 1.0.2 resolution: "iterate-iterator@npm:1.0.2" @@ -37607,6 +37941,13 @@ __metadata: languageName: node linkType: hard +"js-tokens@npm:^9.0.1": + version: 9.0.1 + resolution: "js-tokens@npm:9.0.1" + checksum: 8b604020b1a550e575404bfdde4d12c11a7991ffe0c58a2cf3515b9a512992dc7010af788f0d8b7485e403d462d9e3d3b96c4ff03201550fdbb09e17c811e054 + languageName: node + linkType: hard + "js-yaml@npm:^3.13.1": version: 3.14.1 resolution: "js-yaml@npm:3.14.1" @@ -39004,6 +39345,17 @@ __metadata: languageName: node linkType: hard +"magicast@npm:^0.3.5": + version: 0.3.5 + resolution: "magicast@npm:0.3.5" + dependencies: + "@babel/parser": ^7.25.4 + "@babel/types": ^7.25.4 + source-map-js: ^1.2.0 + checksum: 668f07ade907a44bccfc9a9321588473f6d5fa25329aa26b9ad9a3bf87cc2e6f9c482cbdd3e33c0b9ab9b79c065630c599cc055a12f881c8c924ee0d7282cdce + languageName: node + linkType: hard + "make-dir@npm:^2.0.0, make-dir@npm:^2.1.0": version: 2.1.0 resolution: "make-dir@npm:2.1.0" @@ -39023,6 +39375,15 @@ __metadata: languageName: node linkType: hard +"make-dir@npm:^4.0.0": + version: 4.0.0 + resolution: "make-dir@npm:4.0.0" + dependencies: + semver: ^7.5.3 + checksum: bf0731a2dd3aab4db6f3de1585cea0b746bb73eb5a02e3d8d72757e376e64e6ada190b1eddcde5b2f24a81b688a9897efd5018737d05e02e2a671dda9cff8a8a + languageName: node + linkType: hard + "make-error@npm:1.x, make-error@npm:^1.1.1": version: 1.3.6 resolution: "make-error@npm:1.3.6" @@ -40935,6 +41296,18 @@ __metadata: languageName: node linkType: hard +"mlly@npm:^1.7.4": + version: 1.7.4 + resolution: "mlly@npm:1.7.4" + dependencies: + acorn: ^8.14.0 + pathe: ^2.0.1 + pkg-types: ^1.3.0 + ufo: ^1.5.4 + checksum: a290da940d208f9d77ceed7ed1db3397e37ff083d28bf75e3c92097a8e58967a2b2e2bea33fdcdc63005e2987854cd081dd0621461d89eee4b61c977b5fa020c + languageName: node + linkType: hard + "module-details-from-path@npm:^1.0.3": version: 1.0.3 resolution: "module-details-from-path@npm:1.0.3" @@ -41471,7 +41844,7 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2.0.0, node-fetch@npm:^2.2.0, node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7": +"node-fetch@npm:^2.0.0, node-fetch@npm:^2.2.0, node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7, node-fetch@npm:^2.7.0": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" dependencies: @@ -42902,7 +43275,7 @@ __metadata: languageName: node linkType: hard -"pathe@npm:^2.0.3": +"pathe@npm:^2.0.1, pathe@npm:^2.0.3": version: 2.0.3 resolution: "pathe@npm:2.0.3" checksum: 0602bdd4acb54d91044e0c56f1fb63467ae7d44ab3afea1f797947b0eb2b4d1d91cf0d58d065fdb0a8ab0c4acbbd8d3a5b424983eaf10dd5285d37a16f6e3ee9 @@ -43157,6 +43530,17 @@ __metadata: languageName: node linkType: hard +"pkg-types@npm:^1.3.0": + version: 1.3.1 + resolution: "pkg-types@npm:1.3.1" + dependencies: + confbox: ^0.1.8 + mlly: ^1.7.4 + pathe: ^2.0.1 + checksum: 4fa4edb2bb845646cdbd04c5c6bc43cdbc8f02ed4d1c28bfcafb6e65928aece789bcf1335e4cac5f65dfdc376e4bd7435bd509a35e9ec73ef2c076a1b88e289c + languageName: node + linkType: hard + "pkg-up@npm:^3.1.0": version: 3.1.0 resolution: "pkg-up@npm:3.1.0" @@ -48829,7 +49213,7 @@ __metadata: languageName: node linkType: hard -"source-map-js@npm:^1.2.1": +"source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1": version: 1.2.1 resolution: "source-map-js@npm:1.2.1" checksum: 4eb0cd997cdf228bc253bcaff9340afeb706176e64868ecd20efbe6efea931465f43955612346d6b7318789e5265bdc419bc7669c1cebe3db0eb255f57efa76b @@ -50529,6 +50913,17 @@ __metadata: languageName: node linkType: hard +"test-exclude@npm:^7.0.1": + version: 7.0.1 + resolution: "test-exclude@npm:7.0.1" + dependencies: + "@istanbuljs/schema": ^0.1.2 + glob: ^10.4.1 + minimatch: ^9.0.4 + checksum: e5a49a054bf2da74467dd8149b202166e36275c0dc2c9585f7d34de99c6d055d2287ac8d2a8e4c27c59b893acbc671af3fa869e8069a58ad117250e9c01c726b + languageName: node + linkType: hard + "text-encoding-utf-8@npm:^1.0.2": version: 1.0.2 resolution: "text-encoding-utf-8@npm:1.0.2" @@ -51126,6 +51521,48 @@ __metadata: languageName: node linkType: hard +"tsup@npm:^8.5.0": + version: 8.5.0 + resolution: "tsup@npm:8.5.0" + dependencies: + bundle-require: ^5.1.0 + cac: ^6.7.14 + chokidar: ^4.0.3 + consola: ^3.4.0 + debug: ^4.4.0 + esbuild: ^0.25.0 + fix-dts-default-cjs-exports: ^1.0.0 + joycon: ^3.1.1 + picocolors: ^1.1.1 + postcss-load-config: ^6.0.1 + resolve-from: ^5.0.0 + rollup: ^4.34.8 + source-map: 0.8.0-beta.0 + sucrase: ^3.35.0 + tinyexec: ^0.3.2 + tinyglobby: ^0.2.11 + tree-kill: ^1.2.2 + peerDependencies: + "@microsoft/api-extractor": ^7.36.0 + "@swc/core": ^1 + postcss: ^8.4.12 + typescript: ">=4.5.0" + peerDependenciesMeta: + "@microsoft/api-extractor": + optional: true + "@swc/core": + optional: true + postcss: + optional: true + typescript: + optional: true + bin: + tsup: dist/cli-default.js + tsup-node: dist/cli-node.js + checksum: 5d7083f777fc480fd085ab9060e4223e753f261cbfc87ee284b151f5ccc540b9de8e7792654982e8962d8c5f82c6486cd066754e78984cc761e027c560b81d73 + languageName: node + linkType: hard + "tsutils@npm:^3.21.0": version: 3.21.0 resolution: "tsutils@npm:3.21.0" @@ -51736,6 +52173,13 @@ __metadata: languageName: node linkType: hard +"ufo@npm:^1.5.4": + version: 1.6.1 + resolution: "ufo@npm:1.6.1" + checksum: 2c401dd45bd98ad00806e044aa8571aa2aa1762fffeae5e78c353192b257ef2c638159789f119e5d8d5e5200e34228cd1bbde871a8f7805de25daa8576fb1633 + languageName: node + linkType: hard + "uglify-es@npm:^3.1.9": version: 3.3.10 resolution: "uglify-es@npm:3.3.10" @@ -52499,6 +52943,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:^11.1.0": + version: 11.1.0 + resolution: "uuid@npm:11.1.0" + bin: + uuid: dist/esm/bin/uuid + checksum: 840f19758543c4631e58a29439e51b5b669d5f34b4dd2700b6a1d15c5708c7a6e0c3e2c8c4a2eae761a3a7caa7e9884d00c86c02622ba91137bd3deade6b4b4a + languageName: node + linkType: hard + "uuid@npm:^3.3.2, uuid@npm:^3.4.0": version: 3.4.0 resolution: "uuid@npm:3.4.0"