UNPKG

@gemini-wallet/core

Version:

Core SDK for Gemini Wallet integration with popup communication

1 lines 87.5 kB
{"version":3,"sources":["../src/index.ts","../src/communicator.ts","../src/constants.ts","../package.json","../src/types.ts","../src/utils/base64.ts","../src/utils/calculateWalletAddress.ts","../src/utils/ens.ts","../src/utils/popup.ts","../src/utils/strings.ts","../src/provider/provider.ts","../src/storage/storage.ts","../src/storage/storageInterface.ts","../src/wallets/wallet.ts","../src/provider/provider.utils.ts"],"sourcesContent":["// Main exports\nexport { Communicator } from \"./communicator\";\n\n// Provider exports\nexport { GeminiWalletProvider } from \"./provider\";\nexport * from \"./provider/provider.utils\";\n\n// Wallet exports\nexport { GeminiWallet, isChainSupportedByGeminiSw } from \"./wallets\";\n\n// Storage exports\nexport type { GeminiStorageConfig, IStorage } from \"./storage\";\nexport {\n GeminiStorage,\n STORAGE_CALL_BATCHES_KEY,\n STORAGE_ETH_ACCOUNTS_KEY,\n STORAGE_ETH_ACTIVE_CHAIN_KEY,\n STORAGE_PASSKEY_CREDENTIAL_KEY,\n STORAGE_PRESERVED_PASSKEY_CREDENTIALS_KEY,\n STORAGE_SETTINGS_KEY,\n STORAGE_SMART_ACCOUNT_KEY,\n STORAGE_WC_REQUESTS_KEY,\n} from \"./storage\";\n\n// Type exports\nexport type {\n AppContext,\n AppMetadata,\n Call,\n CallBatchMetadata,\n Chain,\n ConnectResponse,\n GeminiProviderConfig,\n GeminiSdkAppContextMessage,\n GeminiSdkMessage,\n GeminiSdkMessageResponse,\n GeminiSdkSendBatchCalls,\n GeminiSdkSendTransaction,\n GeminiSdkSignMessage,\n GeminiSdkSignTypedData,\n GeminiSdkSwitchChain,\n GetCallsStatusResponse,\n ProviderEventCallback,\n ProviderEventMap,\n ProviderInterface,\n ProviderRpcError,\n ReverseEnsResponse,\n RpcRequestArgs,\n SendCallsParams,\n SendCallsResponse,\n SendTransactionResponse,\n SignMessageResponse,\n SignTypedDataResponse,\n SwitchChainResponse,\n WalletCapabilities,\n} from \"./types\";\nexport { GeminiSdkEvent, PlatformType, ProviderEventEmitter } from \"./types\";\n\n// Utility exports\nexport type { CalculateWalletAddressParams, WebAuthnValidatorData } from \"./utils\";\nexport {\n base64ToHex,\n bufferToBase64URLString,\n calculateV1Address,\n calculateWalletAddress,\n closePopup,\n decodeBase64,\n encodeBase64,\n generateAuthenticatorIdHash,\n hexStringFromNumber,\n openPopup,\n reverseResolveEns,\n safeJsonStringify,\n utf8StringToBuffer,\n validateWebAuthnKey,\n} from \"./utils\";\n\n// Constants\nexport { DEFAULT_CHAIN_ID, POPUP_HEIGHT, POPUP_WIDTH, SDK_BACKEND_URL, SDK_VERSION } from \"./constants\";\n","import { providerErrors, rpcErrors } from \"@metamask/rpc-errors\";\n\nimport { DEFAULT_CHAIN_ID } from \"./constants\";\nimport {\n AppContext,\n type AppMetadata,\n GeminiSdkEvent,\n type GeminiSdkMessage,\n type GeminiSdkMessageResponse,\n} from \"./types\";\nimport { closePopup, openPopup, SDK_BACKEND_URL, SDK_VERSION } from \"./utils\";\n\ntype CommunicatorConfigParams = {\n appMetadata: AppMetadata;\n onDisconnectCallback?: () => void;\n};\n\n// creates and communicates with a popup window to send and receive messages\nexport class Communicator {\n private readonly appMetadata: AppMetadata;\n private readonly url: URL;\n private popup: Window | null = null;\n private listeners = new Map<(_: MessageEvent) => void, { reject: (_: Error) => void }>();\n private onDisconnectCallback?: () => void;\n\n constructor({ appMetadata, onDisconnectCallback }: CommunicatorConfigParams) {\n this.url = new URL(SDK_BACKEND_URL);\n this.appMetadata = appMetadata;\n this.onDisconnectCallback = onDisconnectCallback;\n }\n\n // posts a message to the popup window\n postMessage = async (message: GeminiSdkMessage) => {\n const popup = await this.waitForPopupLoaded();\n popup.postMessage(message, this.url.origin);\n };\n\n // posts a request to the popup window and waits for a response\n postRequestAndWaitForResponse = async <M extends GeminiSdkMessage, R extends GeminiSdkMessageResponse>(\n request: GeminiSdkMessage,\n ): Promise<R> => {\n const responsePromise = this.onMessage<M, R>(({ requestId }) => requestId === request.requestId);\n this.postMessage(request);\n return await responsePromise;\n };\n\n // listens for messages from the popup window that match a given predicate\n onMessage = <M extends GeminiSdkMessage, R extends GeminiSdkMessageResponse>(\n predicate: (_: Partial<M>) => boolean,\n ): Promise<R> => {\n return new Promise((resolve, reject) => {\n const listener = (event: MessageEvent<M>) => {\n // ensure origin of message\n if (event.origin !== this.url.origin) return;\n\n const message = event.data;\n if (predicate(message)) {\n resolve(message as unknown as R);\n window.removeEventListener(\"message\", listener);\n this.listeners.delete(listener);\n }\n };\n\n window.addEventListener(\"message\", listener);\n this.listeners.set(listener, { reject });\n });\n };\n\n // closes the popup, rejects all requests and clears event listeners\n private onRequestCancelled = () => {\n closePopup(this.popup);\n this.popup = null;\n\n this.listeners.forEach(({ reject }, listener) => {\n reject(providerErrors.userRejectedRequest());\n window.removeEventListener(\"message\", listener);\n });\n this.listeners.clear();\n };\n\n // waits for the popup window to fully load and then sends a version message\n waitForPopupLoaded = (): Promise<Window> => {\n if (this.popup && !this.popup.closed) {\n // in case the user un-focused the popup between requests, focus it again\n this.popup.focus();\n return Promise.resolve(this.popup);\n }\n\n this.popup = openPopup(this.url);\n\n // setup popup closed listener in case user closes window without explicit response\n this.onMessage<GeminiSdkMessage, GeminiSdkMessageResponse>(({ event }) => event === GeminiSdkEvent.POPUP_UNLOADED)\n .then(this.onRequestCancelled)\n .catch(() => {});\n\n // setup account disconnect listener in case user requests disconnect from within popup\n this.onMessage<GeminiSdkMessage, GeminiSdkMessageResponse>(({ event }) => event === GeminiSdkEvent.SDK_DISCONNECT)\n .then(() => {\n // invoke disconnect callback passed in from wallet\n this.onDisconnectCallback?.();\n // cleanup remaining event listeners\n this.onRequestCancelled();\n })\n .catch(() => {});\n\n return this.onMessage<GeminiSdkMessage, GeminiSdkMessageResponse>(\n ({ event }) => event === GeminiSdkEvent.POPUP_LOADED,\n )\n .then(message => {\n // report app metadata to backend upon load complete\n this.postMessage({\n chainId: DEFAULT_CHAIN_ID,\n data: {\n appMetadata: this.appMetadata,\n origin: window.location.origin,\n sdkVersion: SDK_VERSION,\n } as AppContext,\n event: GeminiSdkEvent.POPUP_APP_CONTEXT,\n origin: window.location.origin,\n requestId: message.requestId,\n });\n })\n .then(() => {\n if (!this.popup) throw rpcErrors.internal();\n return this.popup;\n });\n };\n}\n","import {\n arbitrum,\n arbitrumSepolia,\n base,\n baseSepolia,\n mainnet,\n optimism,\n optimismSepolia,\n polygon,\n polygonAmoy,\n sepolia,\n} from \"viem/chains\";\n\nimport packageJson from \"../package.json\";\n\nconst DEFAULT_BACKEND_URL = \"https://keys.gemini.com\";\n\nexport const SDK_BACKEND_URL = undefined || DEFAULT_BACKEND_URL;\nexport const ENS_API_URL = \"https://horizon-api.gemini.com/api/ens\";\nexport const SDK_VERSION = packageJson.version;\nexport const DEFAULT_CHAIN_ID = 42161; // Arbitrum One\n\n// Mainnet chain IDs\nexport const MAINNET_CHAIN_IDS = {\n ARBITRUM_ONE: 42161,\n BASE: 8453,\n ETHEREUM: 1,\n OP_MAINNET: 10,\n POLYGON: 137,\n} as const;\n\n// Testnet chain IDs\nexport const TESTNET_CHAIN_IDS = {\n ARBITRUM_SEPOLIA: 421614,\n BASE_SEPOLIA: 84532,\n OP_SEPOLIA: 11155420,\n POLYGON_AMOY: 80002,\n SEPOLIA: 11155111,\n} as const;\n\n// All supported chain IDs\nexport const SUPPORTED_CHAIN_IDS = [...Object.values(MAINNET_CHAIN_IDS), ...Object.values(TESTNET_CHAIN_IDS)];\n\n// Helper function to get default RPC URL for a chain using viem chains\nexport function getDefaultRpcUrl(chainId: number): string | undefined {\n const chainMap: Record<number, string> = {\n [mainnet.id]: mainnet.rpcUrls.default.http[0],\n [arbitrum.id]: arbitrum.rpcUrls.default.http[0],\n [optimism.id]: optimism.rpcUrls.default.http[0],\n [base.id]: base.rpcUrls.default.http[0],\n [polygon.id]: polygon.rpcUrls.default.http[0],\n [sepolia.id]: sepolia.rpcUrls.default.http[0],\n [arbitrumSepolia.id]: arbitrumSepolia.rpcUrls.default.http[0],\n [optimismSepolia.id]: optimismSepolia.rpcUrls.default.http[0],\n [baseSepolia.id]: baseSepolia.rpcUrls.default.http[0],\n [polygonAmoy.id]: polygonAmoy.rpcUrls.default.http[0],\n };\n\n return chainMap[chainId];\n}\n\n// Popup window dimensions\nexport const POPUP_WIDTH = 420;\nexport const POPUP_HEIGHT = 650;\n","{\n \"name\": \"@gemini-wallet/core\",\n \"version\": \"0.3.2\",\n \"description\": \"Core SDK for Gemini Wallet integration with popup communication\",\n \"main\": \"./dist/index.cjs\",\n \"types\": \"./dist/index.d.ts\",\n \"type\": \"module\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/gemini/gemini-wallet-core.git\"\n },\n \"homepage\": \"https://keys.gemini.com\",\n \"bugs\": {\n \"url\": \"https://github.com/gemini/gemini-wallet-core/issues\"\n },\n \"license\": \"MIT\",\n \"author\": \"Gemini\",\n \"files\": [\n \"dist\",\n \"src\",\n \"README.md\",\n \"LICENSE\"\n ],\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n },\n \"./package.json\": \"./package.json\"\n },\n \"scripts\": {\n \"build\": \"dotenv -e .env.production -- tsup\",\n \"dev\": \"dotenv -e .env.local -- tsup --watch\",\n \"typecheck\": \"tsc --noEmit\",\n \"lint\": \"eslint ./src\",\n \"lint:ci\": \"eslint --max-warnings 0 ./src\",\n \"lint:fix\": \"eslint ./src --fix\",\n \"test\": \"bun test\"\n },\n \"dependencies\": {\n \"@metamask/rpc-errors\": \"7.0.2\",\n \"eventemitter3\": \"5.0.1\"\n },\n \"devDependencies\": {\n \"@eslint/eslintrc\": \"3.3.1\",\n \"@eslint/js\": \"9.38.0\",\n \"@types/node\": \"22.13.0\",\n \"dotenv-cli\": \"10.0.0\",\n \"esbuild-plugin-replace\": \"1.4.0\",\n \"eslint\": \"9.38.0\",\n \"eslint-config-prettier\": \"10.1.8\",\n \"eslint-config-turbo\": \"2.5.6\",\n \"eslint-plugin-import\": \"2.32.0\",\n \"eslint-plugin-only-warn\": \"1.1.0\",\n \"eslint-plugin-prettier\": \"5.5.4\",\n \"eslint-plugin-simple-import-sort\": \"12.1.1\",\n \"eslint-plugin-sort-keys-fix\": \"1.1.2\",\n \"globals\": \"16.4.0\",\n \"prettier\": \"3.6.2\",\n \"tsup\": \"8.5.0\",\n \"typescript\": \"5.5.3\",\n \"typescript-eslint\": \"8.40.0\",\n \"vitest\": \"3.2.4\"\n },\n \"peerDependencies\": {\n \"viem\": \">=2.0.0\"\n },\n \"keywords\": [\n \"gemini\",\n \"wallet\",\n \"sdk\",\n \"ethereum\",\n \"web3\",\n \"crypto\"\n ],\n \"module\": \"./dist/index.js\"\n}","import { EventEmitter } from \"eventemitter3\";\nimport type { Address, Hex, SignMessageParameters, SignTypedDataParameters, TransactionRequest } from \"viem\";\n\nimport { type IStorage } from \"./storage/storageInterface\";\n\nexport enum GeminiSdkEvent {\n // Popup events\n POPUP_LOADED = \"POPUP_LOADED\",\n POPUP_UNLOADED = \"POPUP_UNLOADED\",\n POPUP_APP_CONTEXT = \"POPUP_APP_CONTEXT\",\n\n // SDK events\n SDK_CONNECT = \"SDK_CONNECT\",\n SDK_DISCONNECT = \"SDK_DISCONNECT\",\n SDK_SEND_TRANSACTION = \"SDK_SEND_TRANSACTION\",\n SDK_SIGN_DATA = \"SDK_SIGN_DATA\",\n SDK_SIGN_TYPED_DATA = \"SDK_SIGN_TYPED_DATA\",\n SDK_SWITCH_CHAIN = \"SDK_SWITCH_CHAIN\",\n SDK_OPEN_SETTINGS = \"SDK_OPEN_SETTINGS\",\n SDK_CURRENT_ACCOUNT = \"SDK_CURRENT_ACCOUNT\",\n\n // EIP-5792 events\n SDK_SEND_BATCH_CALLS = \"SDK_SEND_BATCH_CALLS\",\n SDK_GET_CAPABILITIES = \"SDK_GET_CAPABILITIES\",\n SDK_GET_CALLS_STATUS = \"SDK_GET_CALLS_STATUS\",\n SDK_SHOW_CALLS_STATUS = \"SDK_SHOW_CALLS_STATUS\",\n}\n\nexport interface AppMetadata {\n /**\n * The name of your application\n */\n name?: string;\n /**\n * The description of your application (optional)\n */\n description?: string;\n /**\n * URL of your application\n */\n url?: string;\n /**\n * URL to your application's icon or logo\n */\n icon?: string;\n /**\n * @deprecated Use `name` instead\n */\n appName?: string;\n /**\n * @deprecated Use `icon` instead\n */\n appLogoUrl?: string;\n}\n\nexport interface AppContext {\n appMetadata: AppMetadata;\n origin: string;\n sdkVersion: string;\n}\n\nexport interface Chain {\n id: number;\n rpcUrl?: string;\n}\n\n// Using const object with 'as const' assertion instead of enum\n// This avoids TypeScript's isolatedModules re-export limitations\nexport const PlatformType = {\n REACT_NATIVE: \"REACT_NATIVE\",\n WEB: \"WEB\",\n} as const;\n\n// Extract type from const object for type safety\nexport type PlatformType = (typeof PlatformType)[keyof typeof PlatformType];\n\nexport type GeminiProviderConfig = {\n appMetadata: AppMetadata;\n chain: Chain;\n platform?: PlatformType;\n onDisconnectCallback?: () => void;\n storage?: IStorage;\n};\n\nexport interface RpcRequestArgs {\n readonly method: string;\n readonly params?: readonly unknown[] | object | Hex[];\n}\n\nexport interface ProviderRpcError extends Error {\n code: number;\n data?: unknown;\n message: string;\n}\n\nexport type ProviderEventMap = {\n accountsChanged: string[];\n chainChanged: string; // hex string\n connect: {\n readonly chainId: string;\n };\n disconnect: ProviderRpcError;\n};\n\nexport type ProviderEventCallback = ProviderInterface[\"emit\"];\n\nexport class ProviderEventEmitter extends EventEmitter<keyof ProviderEventMap> {}\n\nexport interface ProviderInterface extends ProviderEventEmitter {\n disconnect(): Promise<void>;\n emit<K extends keyof ProviderEventMap>(event: K, ...args: [ProviderEventMap[K]]): boolean;\n on<K extends keyof ProviderEventMap>(event: K, listener: (_: ProviderEventMap[K]) => void): this;\n request(args: RpcRequestArgs): Promise<any>;\n}\n\nexport interface GeminiSdkMessage {\n chainId: number;\n data?: unknown;\n event: GeminiSdkEvent;\n origin: string;\n requestId?: string;\n wcData?: any;\n}\n\nexport interface GeminiSdkMessageResponse {\n data?: unknown;\n event: GeminiSdkEvent;\n requestId?: string;\n}\n\nexport interface ConnectResponse extends Omit<GeminiSdkMessageResponse, \"data\"> {\n data: { address: Address };\n}\n\nexport interface SendTransactionResponse extends Omit<GeminiSdkMessageResponse, \"data\"> {\n data: { hash?: Hex; error?: string };\n}\n\nexport interface SignMessageResponse extends Omit<GeminiSdkMessageResponse, \"data\"> {\n data: { hash?: Hex; error?: string };\n}\n\nexport interface SignTypedDataResponse extends Omit<GeminiSdkMessageResponse, \"data\"> {\n data: { hash?: Hex; error?: string };\n}\n\nexport interface SwitchChainResponse extends Omit<GeminiSdkMessageResponse, \"data\"> {\n data: { chainId?: number; error?: string };\n}\n\nexport interface GeminiSdkSendTransaction extends Omit<GeminiSdkMessage, \"data\"> {\n data: TransactionRequest;\n}\n\nexport interface GeminiSdkSignMessage extends Omit<GeminiSdkMessage, \"data\"> {\n data: SignMessageParameters;\n}\n\nexport interface GeminiSdkSignTypedData extends Omit<GeminiSdkMessage, \"data\"> {\n data: SignTypedDataParameters;\n}\n\nexport interface GeminiSdkSendBatchCalls extends Omit<GeminiSdkMessage, \"data\"> {\n data: SendCallsParams;\n}\n\nexport interface GeminiSdkSwitchChain extends Omit<GeminiSdkMessage, \"data\"> {\n data: number;\n}\n\nexport interface GeminiSdkAppContextMessage extends Omit<GeminiSdkMessage, \"data\"> {\n data: AppContext;\n}\n\nexport interface ReverseEnsResponse {\n address: Address;\n name: string | null;\n}\n\n// EIP-5792 Types\nexport interface Call {\n to: Address;\n value?: Hex;\n data?: Hex;\n chainId?: Hex;\n}\n\nexport interface SendCallsParams {\n version: string;\n chainId: Hex;\n from: Address;\n calls: Call[];\n capabilities?: Record<string, any>;\n}\n\nexport interface WalletCapabilities {\n [chainId: string]: {\n atomic?: {\n status: \"supported\" | \"unsupported\";\n };\n paymasterService?: {\n supported: boolean;\n };\n };\n}\n\nexport interface CallBatchMetadata {\n id: string;\n chainId: string;\n from: Address;\n calls: Call[];\n transactionHash?: Hex;\n status: \"pending\" | \"confirmed\" | \"failed\" | \"reverted\";\n timestamp: number;\n capabilities?: Record<string, any>;\n}\n\nexport interface GetCallsStatusResponse {\n version: string;\n id: string;\n chainId: Hex;\n status: 100 | 200 | 400 | 500; // pending, confirmed, offchain failure, reverted\n atomic: boolean;\n receipts?: Array<{\n logs: Array<{\n address: Address;\n data: Hex;\n topics: Hex[];\n }>;\n status: \"success\" | \"reverted\";\n blockHash: Hex;\n blockNumber: Hex;\n gasUsed: Hex;\n transactionHash: Hex;\n }>;\n}\n\nexport interface SendCallsResponse {\n id: string;\n capabilities?: {\n caip345?: {\n caip2: string;\n transactionHashes: Hex[];\n };\n };\n}\n","/**\n * Utility functions for base64 encoding and decoding\n * Compatible with both browser and Node.js environments\n */\n\n/**\n * Encodes a Uint8Array to a base64url string\n * @param array - The Uint8Array to encode\n * @returns The base64url encoded string\n */\nexport function encodeBase64(array: Uint8Array): string {\n let base64: string;\n\n // Check if we're in a Node.js environment (Buffer is available)\n if (typeof Buffer !== \"undefined\") {\n // Node.js environment\n base64 = Buffer.from(array).toString(\"base64\");\n } else {\n // Browser environment\n base64 = btoa(\n Array.from(array)\n .map(b => String.fromCharCode(b))\n .join(\"\"),\n );\n }\n\n // Convert to base64url format by replacing characters\n return base64.replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n/**\n * Decodes a base64url string to a Uint8Array\n * @param base64url - The base64url encoded string\n * @returns The decoded Uint8Array\n */\nexport function decodeBase64(base64url: string): Uint8Array {\n // Convert base64url to standard base64 by restoring special chars\n let base64 = base64url.replace(/-/g, \"+\").replace(/_/g, \"/\");\n\n // Add padding if needed\n while (base64.length % 4 !== 0) {\n base64 += \"=\";\n }\n\n // Check if we're in a Node.js environment (Buffer is available)\n if (typeof Buffer !== \"undefined\") {\n // Node.js environment\n return new Uint8Array(Buffer.from(base64, \"base64\"));\n } else {\n // Browser environment\n const binaryString = atob(base64);\n const bytes = new Uint8Array(binaryString.length);\n for (let i = 0; i < binaryString.length; i++) {\n bytes[i] = binaryString.charCodeAt(i);\n }\n return bytes;\n }\n}\n\n/**\n * Convert an ArrayBuffer or Uint8Array to a base64url string\n * @param buffer - The buffer to convert\n * @returns The base64url encoded string\n */\nexport function bufferToBase64URLString(buffer: ArrayBuffer | Uint8Array): string {\n const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);\n return encodeBase64(bytes);\n}\n\n/**\n * Convert a string to UTF-8 encoded Uint8Array\n * @param value - The string to convert\n * @returns The UTF-8 encoded Uint8Array\n */\nexport function utf8StringToBuffer(value: string): Uint8Array {\n if (typeof TextEncoder !== \"undefined\") {\n // Modern browsers and Node.js with TextEncoder support\n return new TextEncoder().encode(value);\n } else if (typeof Buffer !== \"undefined\") {\n // Node.js fallback\n return new Uint8Array(Buffer.from(value, \"utf8\"));\n } else {\n // Very old browsers fallback (not recommended)\n const bytes = new Uint8Array(value.length);\n for (let i = 0; i < value.length; i++) {\n bytes[i] = value.charCodeAt(i);\n }\n return bytes;\n }\n}\n\n/**\n * Convert a base64 string to hex string\n * @param base64 - The base64 string to convert\n * @returns The hex string\n */\nexport function base64ToHex(base64: string): string {\n const bytes = decodeBase64(base64);\n return Array.from(bytes)\n .map(b => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n","import {\n type Address,\n encodeAbiParameters,\n encodeFunctionData,\n encodePacked,\n getCreate2Address,\n type Hex,\n keccak256,\n} from \"viem\";\n\n// WebAuthn validator data structure\nexport interface WebAuthnValidatorData {\n pubKeyX: bigint;\n pubKeyY: bigint;\n}\n\n// Parameters for calculating wallet address\nexport interface CalculateWalletAddressParams {\n publicKey: Hex; // Combined 64-byte hex string (32 bytes X + 32 bytes Y)\n credentialId: string; // Base64URL encoded credential ID\n index?: bigint; // Optional, defaults to 0\n}\n\n// Shared contract addresses across versions\nconst SHARED_CONTRACT_ADDRESSES = {\n ATTESTER: \"0x000474392a9cd86a4687354f1Ce2964B52e97484\" as const,\n BOOTSTRAPPER: \"0x00000000D3254452a909E4eeD47455Af7E27C289\" as const,\n REGISTRY: \"0x000000000069E2a187AEFFb852bF3cCdC95151B2\" as const,\n};\n\n// V2 contract addresses (current Horizon deployment)\nconst V2_CONTRACT_ADDRESSES = {\n ...SHARED_CONTRACT_ADDRESSES,\n ACCOUNT_IMPLEMENTATION: \"0x00000000029d9c8b864DD51d6bb0d99FB72D650b\" as const,\n FACTORY: \"0x000000000452377e1Bd9e72E939855ECb9363Cab\" as const,\n WEBAUTHN_VALIDATOR: \"0x7ab16Ff354AcB328452F1D445b3Ddee9a91e9e69\" as const,\n};\n\n// V1 contract addresses\nconst V1_CONTRACT_ADDRESSES = {\n ...SHARED_CONTRACT_ADDRESSES,\n ACCOUNT_IMPLEMENTATION: \"0x0006050168DE255a8672ACaD4821e721CBA44337\" as const,\n FACTORY: \"0x00E58DF70FaB983a324c4C068c82d20407579FaC\" as const,\n WEBAUTHN_VALIDATOR: \"0xbA45a2BFb8De3D24cA9D7F1B551E14dFF5d690Fd\" as const,\n};\n\n/**\n * Internal helper to process and validate wallet address calculation parameters\n */\nfunction processWalletAddressParams(\n params: CalculateWalletAddressParams,\n contractAddresses: typeof V1_CONTRACT_ADDRESSES | typeof V2_CONTRACT_ADDRESSES,\n): Address {\n const { publicKey, credentialId, index = 0n } = params;\n\n // Validate input\n if (!publicKey.startsWith(\"0x\") || publicKey.length !== 130) {\n throw new Error(\"Invalid public key: must be 64-byte hex string (0x + 128 chars)\");\n }\n\n // Extract X and Y coordinates\n const pubKeyX = `0x${publicKey.slice(2, 66)}` as Hex;\n const pubKeyY = `0x${publicKey.slice(66, 130)}` as Hex;\n\n // Convert to WebAuthnValidatorData\n const webAuthnData: WebAuthnValidatorData = {\n pubKeyX: BigInt(pubKeyX),\n pubKeyY: BigInt(pubKeyY),\n };\n\n // Validate the key is on the secp256r1 curve\n if (!validateWebAuthnKey(webAuthnData)) {\n throw new Error(\"Invalid WebAuthn key: coordinates are not on secp256r1 curve\");\n }\n\n // Calculate authenticator ID hash from credential ID\n const authenticatorIdHash = generateAuthenticatorIdHash(credentialId);\n\n // Use the internal calculation with provided addresses\n return calculateAddressInternal({\n authenticatorIdHash,\n contractAddresses,\n index,\n webAuthnData,\n });\n}\n\n/**\n * Calculate smart wallet address from public key and credential ID (V2)\n * This handles all validation and setup internally\n */\nexport function calculateWalletAddress(params: CalculateWalletAddressParams): Address {\n return processWalletAddressParams(params, V2_CONTRACT_ADDRESSES);\n}\n\n/**\n * Calculate smart wallet address from public key and credential ID (V1)\n * This handles all validation and setup internally\n */\nexport function calculateV1Address(params: CalculateWalletAddressParams): Address {\n return processWalletAddressParams(params, V1_CONTRACT_ADDRESSES);\n}\n\n/**\n * Generate authenticator ID hash from credential ID\n */\nexport function generateAuthenticatorIdHash(credentialId: string): Hex {\n // Convert base64url to bytes\n const padding = \"=\".repeat((4 - (credentialId.length % 4)) % 4);\n const base64 = credentialId.replace(/-/g, \"+\").replace(/_/g, \"/\") + padding;\n\n const binaryString = atob(base64);\n const bytes = new Uint8Array(binaryString.length);\n for (let i = 0; i < binaryString.length; i++) {\n bytes[i] = binaryString.charCodeAt(i);\n }\n\n return keccak256(bytes);\n}\n\n/**\n * Validate WebAuthn public key offchain\n * Mirrors the contract's _validateWebAuthnKey function\n */\nexport function validateWebAuthnKey(webAuthnData: WebAuthnValidatorData): boolean {\n const SECP256R1_P = 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffffn;\n const SECP256R1_B = 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604bn;\n\n const { pubKeyX, pubKeyY } = webAuthnData;\n\n // Check if coordinates are valid\n if (pubKeyX === 0n || pubKeyY === 0n || pubKeyX >= SECP256R1_P || pubKeyY >= SECP256R1_P) {\n return false;\n }\n\n // Validate curve membership: Y² ≡ X³ - 3X + B (mod P)\n const ySquared = (pubKeyY * pubKeyY) % SECP256R1_P;\n const xCubed = (pubKeyX * pubKeyX * pubKeyX) % SECP256R1_P;\n const threeX = (3n * pubKeyX) % SECP256R1_P;\n const rightSide = (xCubed + SECP256R1_P - threeX + SECP256R1_B) % SECP256R1_P;\n\n return ySquared === rightSide;\n}\n\n/**\n * Internal calculation method using provided contract addresses\n */\nfunction calculateAddressInternal(params: {\n webAuthnData: WebAuthnValidatorData;\n authenticatorIdHash: Hex;\n index: bigint;\n contractAddresses: typeof V1_CONTRACT_ADDRESSES | typeof V2_CONTRACT_ADDRESSES;\n}): Address {\n const { webAuthnData, authenticatorIdHash, index, contractAddresses } = params;\n\n // Use provided contract addresses\n const factoryAddress = contractAddresses.FACTORY;\n const accountImplementation = contractAddresses.ACCOUNT_IMPLEMENTATION;\n const webAuthnValidator = contractAddresses.WEBAUTHN_VALIDATOR;\n const attester = contractAddresses.ATTESTER;\n const bootstrapper = contractAddresses.BOOTSTRAPPER;\n const registry = contractAddresses.REGISTRY;\n\n // Generate cross-chain consistent salt (same as contract)\n const salt = keccak256(\n encodePacked(\n [\"uint256\", \"uint256\", \"bytes32\", \"uint256\"],\n [webAuthnData.pubKeyX, webAuthnData.pubKeyY, authenticatorIdHash, index],\n ),\n );\n\n // Prepare validator initialization data (WebAuthnValidatorData + authenticatorIdHash)\n const validatorInitData = encodeAbiParameters(\n [\n {\n components: [\n { name: \"pubKeyX\", type: \"uint256\" },\n { name: \"pubKeyY\", type: \"uint256\" },\n ],\n type: \"tuple\",\n },\n { type: \"bytes32\" },\n ],\n [webAuthnData, authenticatorIdHash],\n );\n\n // Create RegistryConfig struct\n const registryConfig = {\n attesters: [attester],\n registry,\n threshold: 1n,\n };\n\n // Encode the bootstrap call\n const bootstrapCall = encodeFunctionData({\n abi: [\n {\n inputs: [\n { name: \"validator\", type: \"address\" },\n { name: \"validatorInitData\", type: \"bytes\" },\n {\n components: [\n { name: \"registry\", type: \"address\" },\n { name: \"attesters\", type: \"address[]\" },\n { name: \"threshold\", type: \"uint8\" },\n ],\n name: \"registryConfig\",\n type: \"tuple\",\n },\n ],\n name: \"initNexusWithSingleValidator\",\n type: \"function\",\n },\n ],\n args: [webAuthnValidator, validatorInitData, registryConfig],\n functionName: \"initNexusWithSingleValidator\",\n });\n\n // Format initialization data as expected by ProxyLib\n const initData = encodeAbiParameters([{ type: \"address\" }, { type: \"bytes\" }], [bootstrapper, bootstrapCall]);\n\n // Calculate CREATE2 address using the same logic as ProxyLib.predictProxyAddress\n return predictProxyAddress(accountImplementation, salt, initData, factoryAddress);\n}\n\n/**\n * Predicts the proxy address using CREATE2\n * Mirrors ProxyLib.predictProxyAddress functionality exactly\n */\nfunction predictProxyAddress(implementation: Address, salt: Hex, initData: Hex, deployer: Address): Address {\n // Encode the call to INexus.initializeAccount with initData\n const initializeCall = encodeFunctionData({\n abi: [\n {\n inputs: [{ name: \"data\", type: \"bytes\" }],\n name: \"initializeAccount\",\n type: \"function\",\n },\n ],\n args: [initData],\n functionName: \"initializeAccount\",\n });\n\n // Encode constructor arguments for NexusProxy\n const constructorArgs = encodeAbiParameters(\n [{ type: \"address\" }, { type: \"bytes\" }],\n [implementation, initializeCall],\n );\n\n // Calculate initCodeHash using actual compiled NexusProxy creation bytecode\n const nexusProxyCreationCode =\n \"0x60806040526102c8803803806100148161018c565b92833981016040828203126101885781516001600160a01b03811692909190838303610188576020810151906001600160401b03821161018857019281601f8501121561018857835161006e610069826101c5565b61018c565b9481865260208601936020838301011161018857815f926020809301865e8601015260017f90b772c2cb8a51aa7a8a65fc23543c6d022d5b3f8e2b92eed79fba7eef8293005d823b15610176577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc80546001600160a01b031916821790557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b5f80a282511561015e575f8091610146945190845af43d15610156573d91610137610069846101c5565b9283523d5f602085013e6101e0565b505b6040516089908161023f8239f35b6060916101e0565b50505034156101485763b398979f60e01b5f5260045ffd5b634c9c8ce360e01b5f5260045260245ffd5b5f80fd5b6040519190601f01601f191682016001600160401b038111838210176101b157604052565b634e487b7160e01b5f52604160045260245ffd5b6001600160401b0381116101b157601f01601f191660200190565b9061020457508051156101f557805190602001fd5b63d6bda27560e01b5f5260045ffd5b81511580610235575b610215575090565b639996b31560e01b5f9081526001600160a01b0391909116600452602490fd5b50803b1561020d56fe608060405236156051577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545f9081906001600160a01b0316368280378136915af43d5f803e15604d573d5ff35b3d5ffd5b00fea264697066735822122041b5f70a351952142223f22504ca7b4e6d975f3a302d114ff820442fcf815ac264736f6c634300081b0033\" as const;\n\n const initCodeHash = keccak256(encodePacked([\"bytes\", \"bytes\"], [nexusProxyCreationCode, constructorArgs]));\n\n // Standard CREATE2 formula\n return getCreate2Address({\n bytecodeHash: initCodeHash,\n from: deployer,\n salt,\n });\n}\n","import type { Address } from \"viem\";\n\nimport { ENS_API_URL } from \"@/constants\";\nimport type { ReverseEnsResponse } from \"@/types\";\n\nexport async function reverseResolveEns(address: Address): Promise<ReverseEnsResponse> {\n try {\n const response = await fetch(`${ENS_API_URL}/reverse/${address}`);\n\n if (!response.ok) {\n throw new Error(`ENS API request failed: ${response.status} ${response.statusText}`);\n }\n\n const data: ReverseEnsResponse = await response.json();\n\n return {\n address: data.address,\n name: data.name || null,\n };\n } catch (error) {\n console.error(\"Failed to resolve ENS name:\", error);\n return {\n address,\n name: null,\n };\n }\n}\n","import { rpcErrors } from \"@metamask/rpc-errors\";\n\nconst POPUP_WIDTH = 420;\nconst POPUP_HEIGHT = 650;\n\nexport const openPopup = (url: URL): Window => {\n const left = (window.innerWidth - POPUP_WIDTH) / 2 + window.screenX;\n const top = (window.innerHeight - POPUP_HEIGHT) / 2 + window.screenY;\n\n const popupId = `wallet_${window?.crypto?.randomUUID()}`;\n const popup = window.open(url, popupId, `width=${POPUP_WIDTH}, height=${POPUP_HEIGHT}, left=${left}, top=${top}`);\n\n popup?.focus();\n\n if (!popup) {\n throw rpcErrors.internal(\"Pop up window failed to open\");\n }\n\n return popup;\n};\n\nexport const closePopup = (popup: Window | null) => {\n if (popup && !popup.closed) {\n popup.opener?.focus();\n popup.close();\n }\n};\n","export const hexStringFromNumber = (num: number): string => {\n return `0x${BigInt(num).toString(16)}`;\n};\n\nexport const safeJsonStringify = (obj: any) =>\n JSON.stringify(obj, (_, value) => (typeof value === \"bigint\" ? value.toString() + \"n\" : value), 2);\n","import { errorCodes, providerErrors, rpcErrors, serializeError } from \"@metamask/rpc-errors\";\nimport {\n type Address,\n type Hex,\n type SignMessageParameters,\n SignTypedDataParameters,\n type TransactionRequest,\n} from \"viem\";\n\nimport { DEFAULT_CHAIN_ID } from \"../constants\";\nimport { GeminiStorage, STORAGE_ETH_ACCOUNTS_KEY, STORAGE_ETH_ACTIVE_CHAIN_KEY } from \"../storage\";\nimport {\n type GeminiProviderConfig,\n type GetCallsStatusResponse,\n ProviderEventEmitter,\n type ProviderInterface,\n type RpcRequestArgs,\n type SendCallsParams,\n type SendCallsResponse,\n type WalletCapabilities,\n} from \"../types\";\nimport { hexStringFromNumber } from \"../utils\";\nimport { GeminiWallet } from \"../wallets\";\nimport { convertSendValuesToBigInt, fetchRpcRequest, validateRpcRequestArgs } from \"./provider.utils\";\n\nexport class GeminiWalletProvider extends ProviderEventEmitter implements ProviderInterface {\n private readonly config: GeminiProviderConfig;\n private wallet: GeminiWallet | null = null;\n\n constructor(providerConfig: Readonly<GeminiProviderConfig>) {\n super();\n this.config = providerConfig;\n\n // Preserve user's disconnect callback while adding provider cleanup\n const userDisconnectCallback = providerConfig.onDisconnectCallback;\n this.wallet = new GeminiWallet({\n ...providerConfig,\n onDisconnectCallback: () => {\n // Call user's callback first\n userDisconnectCallback?.();\n // Then handle provider cleanup\n this.disconnect();\n },\n });\n }\n\n public async request<T>(args: RpcRequestArgs): Promise<T> {\n try {\n validateRpcRequestArgs(args);\n\n if (!this.wallet?.accounts?.length) {\n switch (args.method) {\n case \"eth_requestAccounts\": {\n // Use existing wallet instance instead of recreating\n if (!this.wallet) {\n // Preserve user's disconnect callback while adding provider cleanup\n const userDisconnectCallback = this.config.onDisconnectCallback;\n this.wallet = new GeminiWallet({\n ...this.config,\n onDisconnectCallback: () => {\n // Call user's callback first\n userDisconnectCallback?.();\n // Then handle provider cleanup\n this.disconnect();\n },\n });\n }\n await this.wallet.connect();\n this.emit(\"accountsChanged\", this.wallet.accounts);\n break;\n }\n case \"net_version\":\n // not connected default value\n return DEFAULT_CHAIN_ID as T;\n case \"eth_chainId\":\n // not connected default value\n return hexStringFromNumber(DEFAULT_CHAIN_ID) as T;\n default: {\n // all other methods require active connection\n throw providerErrors.unauthorized();\n }\n }\n }\n\n let response;\n let requestParams;\n switch (args.method) {\n case \"eth_requestAccounts\":\n case \"eth_accounts\":\n response = this.wallet.accounts;\n break;\n case \"net_version\":\n response = this.wallet.chain.id;\n break;\n case \"eth_chainId\":\n response = hexStringFromNumber(this.wallet.chain.id);\n break;\n case \"personal_sign\":\n case \"wallet_sign\":\n requestParams = args.params as Array<Hex | Address>;\n response = await this.wallet.signData({\n account: requestParams[1] as Address,\n message: requestParams[0] as Hex,\n } as SignMessageParameters);\n if (response.error) {\n throw rpcErrors.transactionRejected(response.error);\n } else {\n response = response.hash;\n }\n break;\n case \"eth_sendTransaction\":\n case \"wallet_sendTransaction\":\n requestParams = args.params as Array<TransactionRequest>;\n requestParams = convertSendValuesToBigInt(requestParams[0]);\n response = await this.wallet.sendTransaction(requestParams);\n if (response.error) {\n throw rpcErrors.transactionRejected(response.error);\n } else {\n response = response.hash;\n }\n break;\n case \"wallet_switchEthereumChain\": {\n // Handle both standard EIP-3326 format [{ chainId: hex }] and legacy format { id: number }\n const rawParams = args.params as [{ chainId: string }] | { id: number };\n let chainId: number;\n\n if (Array.isArray(rawParams) && rawParams[0]?.chainId) {\n // Standard EIP-3326 format: [{ chainId: \"0x1\" }]\n chainId = parseInt(rawParams[0].chainId, 16);\n } else if (\n rawParams &&\n typeof rawParams === \"object\" &&\n \"id\" in rawParams &&\n Number.isInteger(rawParams.id)\n ) {\n // Legacy format: { id: 1 }\n chainId = rawParams.id;\n } else {\n throw rpcErrors.invalidParams(\n \"Invalid chain id argument. Expected [{ chainId: hex_string }] or { id: number }.\",\n );\n }\n\n response = await this.wallet.switchChain({ id: chainId });\n\n // Per EIP-3326, a non-null response indicates error\n if (response) {\n throw providerErrors.custom({ code: 4902, message: response });\n }\n\n await this.emit(\"chainChanged\", hexStringFromNumber(chainId));\n break;\n }\n case \"eth_signTypedData_v1\":\n case \"eth_signTypedData_v2\":\n case \"eth_signTypedData_v3\":\n case \"eth_signTypedData_v4\":\n case \"eth_signTypedData\": {\n requestParams = args.params as Array<Hex | Address>;\n const signedTypedDataParams = JSON.parse(requestParams[1] as string) as SignTypedDataParameters;\n response = await this.wallet.signTypedData({\n account: requestParams[0] as Address,\n domain: signedTypedDataParams.domain,\n message: signedTypedDataParams.message,\n primaryType: signedTypedDataParams.primaryType,\n types: signedTypedDataParams.types,\n });\n if (response.error) {\n throw rpcErrors.transactionRejected(response.error);\n } else {\n response = response.hash;\n }\n break;\n }\n // EIP-5792 Wallet Call API\n case \"wallet_getCapabilities\": {\n const capabilityParams = Array.isArray(args.params) ? args.params : undefined;\n response = this.getCapabilities(capabilityParams);\n break;\n }\n case \"wallet_sendCalls\": {\n requestParams = args.params as [SendCallsParams];\n response = await this.sendCalls(requestParams[0]);\n break;\n }\n case \"wallet_getCallsStatus\": {\n requestParams = args.params as [string];\n response = await this.getCallsStatus(requestParams[0]);\n break;\n }\n case \"wallet_showCallsStatus\": {\n requestParams = args.params as [string];\n await this.showCallsStatus(requestParams[0]);\n response = null;\n break;\n }\n\n // TODO: not yet implemented or unclear if we support\n case \"eth_ecRecover\":\n case \"eth_subscribe\":\n case \"eth_unsubscribe\":\n case \"personal_ecRecover\":\n case \"eth_signTransaction\":\n case \"wallet_watchAsset\":\n case \"wallet_grantPermissions\":\n throw rpcErrors.methodNotSupported(\"Not yet implemented.\");\n\n // not supported\n case \"eth_sign\":\n case \"eth_coinbase\":\n case \"wallet_addEthereumChain\":\n throw rpcErrors.methodNotSupported();\n\n // call rpc directly for everything else\n default:\n if (!this.wallet.chain.rpcUrl)\n throw rpcErrors.internal(`RPC URL missing for current chain (${this.wallet.chain.id})`);\n return fetchRpcRequest(args, this.wallet.chain.rpcUrl);\n }\n\n return response as T;\n } catch (error) {\n const { code } = error as { code?: number };\n if (code === errorCodes.provider.unauthorized) this.disconnect();\n return Promise.reject(serializeError(error));\n }\n }\n\n // custom wallet function to open settings page\n async openSettings() {\n await this.wallet?.openSettings();\n }\n\n // EIP-5792 Implementation Methods - delegating to wallet\n\n private getCapabilities(params?: readonly unknown[]): WalletCapabilities {\n if (!this.wallet) {\n throw providerErrors.unauthorized();\n }\n const requestedChainIds = params?.[0] as string[] | undefined;\n return this.wallet.getCapabilities(requestedChainIds);\n }\n\n private async sendCalls(params: SendCallsParams): Promise<SendCallsResponse> {\n if (!this.wallet) {\n throw providerErrors.unauthorized();\n }\n try {\n return await this.wallet.sendCalls(params);\n } catch (error) {\n throw rpcErrors.transactionRejected(error instanceof Error ? error.message : String(error));\n }\n }\n\n private async getCallsStatus(batchId: string): Promise<GetCallsStatusResponse> {\n if (!this.wallet) {\n throw providerErrors.unauthorized();\n }\n try {\n return await this.wallet.getCallsStatus(batchId);\n } catch (error) {\n throw rpcErrors.invalidParams(error instanceof Error ? error.message : String(error));\n }\n }\n\n private async showCallsStatus(batchId: string): Promise<void> {\n if (!this.wallet) {\n throw providerErrors.unauthorized();\n }\n try {\n await this.wallet.showCallsStatus(batchId);\n } catch (error) {\n throw rpcErrors.invalidParams(error instanceof Error ? error.message : String(error));\n }\n }\n\n async disconnect() {\n // If wallet exists, let it handle its own storage cleanup\n if (this.wallet) {\n // Create a temporary storage instance with the same config to clean up\n const storage = this.config.storage || new GeminiStorage();\n await storage.removeItem(STORAGE_ETH_ACCOUNTS_KEY);\n await storage.removeItem(STORAGE_ETH_ACTIVE_CHAIN_KEY);\n }\n this.wallet = null;\n // Call the user's disconnect callback if provided\n this.config.onDisconnectCallback?.();\n await this.emit(\"disconnect\", \"User initiated disconnection\");\n await this.emit(\"accountsChanged\", []);\n }\n}\n","import { safeJsonStringify } from \"../utils\";\nimport { type IStorage } from \"./storageInterface\";\n\n// memory fallback storage for environments without localStorage\nconst memoryStorage: Record<string, string> = {};\n\nexport type GeminiStorageConfig = {\n scope?: string;\n module?: string;\n};\n\n/**\n * Default web storage implementation using localStorage\n * For mobile platforms, implement a custom storage class that implements IStorage\n */\nexport class GeminiStorage implements IStorage {\n private scope: string;\n private module: string;\n\n constructor({ scope = \"@gemini\", module = \"wallet\" }: GeminiStorageConfig = {}) {\n this.scope = scope;\n this.module = module;\n }\n\n private scopedKey(key: string): string {\n return `${this.scope}.${this.module}.${key}`;\n }\n\n public async storeObject<T>(key: string, item: T): Promise<void> {\n const json = safeJsonStringify(item);\n await this.setItem(key, json);\n }\n\n public async loadObject<T>(key: string, fallback: T): Promise<T> {\n const item = await this.getItem(key);\n if (!item) {\n await this.storeObject(key, fallback);\n return fallback;\n }\n\n try {\n return JSON.parse(item);\n } catch (error) {\n console.error(`Error parsing JSON for key ${key}:`, error);\n return fallback;\n }\n }\n\n // eslint-disable-next-line require-await\n public async setItem(key: string, value: string): Promise<void> {\n const scoped = this.scopedKey(key);\n\n try {\n localStorage.setItem(scoped, value);\n } catch (e) {\n // fallback to memory storage if localStorage is not available\n console.warn(\"localStorage not available, using memory storage\", e);\n memoryStorage[scoped] = value;\n }\n }\n\n // eslint-disable-next-line require-await\n public async getItem(key: string): Promise<string | null> {\n const scoped = this.scopedKey(key);\n\n try {\n return localStorage.getItem(scoped);\n } catch (e) {\n // fallback to memory storage if localStorage is not available\n console.warn(\"localStorage not available, using memory storage\", e);\n return memoryStorage[scoped] || null;\n }\n }\n\n // eslint-disable-next-line require-await\n public async removeItem(key: string): Promise<void> {\n const scoped = this.scopedKey(key);\n\n try {\n localStorage.removeItem(scoped);\n } catch (e) {\n // fallback to memory storage if localStorage is not available\n console.warn(\"localStorage not available, using memory storage\", e);\n delete memoryStorage[scoped];\n }\n }\n\n public async removeItems(keys: string[]): Promise<void> {\n await Promise.all(keys.map(key => this.removeItem(key)));\n }\n}\n","/**\n * Interface for storage backends used by the Gemini wallet SDK\n */\nexport interface IStorage {\n /**\n * Store a serializable object in storage\n * @param key Storage key\n * @param item Object to store\n */\n storeObject<T>(key: string, item: T): Promise<void>;\n\n /**\n * Load a serializable object from storage\n * @param key Storage key\n * @param fallback Default value if key doesn't exist\n * @returns The stored object or fallback\n */\n loadObject<T>(key: string, fallback: T): Promise<T>;\n\n /**\n * Store a string value in storage\n * @param key Storage key\n * @param value String value to store\n */\n setItem(key: string, value: string): Promise<void>;\n\n /**\n * Retrieve a string value from storage\n * @param key Storage key\n * @returns The stored string or null if not found\n */\n getItem(key: string): Promise<string | null>;\n\n /**\n * Remove an item from storage\n * @param key Storage key\n */\n removeItem(key: string): Promise<void>;\n\n /**\n * Remove multiple items from storage\n * @param keys Array of storage keys to remove\n */\n removeItems(keys: string[]): Promise<void>;\n}\n\n// Export storage keys\nexport const STORAGE_ETH_ACCOUNTS_KEY = \"eth-accounts\";\nexport const STORAGE_ETH_ACTIVE_CHAIN_KEY = \"eth-active-chain\";\nexport const STORAGE_PASSKEY_CREDENTIAL_KEY = \"passkey-credential\";\nexport const STORAGE_PRESERVED_PASSKEY_CREDENTIALS_KEY = \"preserved-passkey-credentials\";\nexport const STORAGE_SMART_ACCOUNT_KEY = \"smart-account\";\nexport const STORAGE_SETTINGS_KEY = \"settings\";\nexport const STORAGE_WC_REQUESTS_KEY = \"wc-requests\";\nexport const STORAGE_CALL_BATCHES_KEY = \"call-batches\";\n","import {\n type Address,\n type Hex,\n type SignMessageParameters,\n type SignTypedDataParameters,\n type SwitchChainParameters,\n type TransactionRequest,\n} from \"viem\";\n\nimport { Communicator } from \"../communicator\";\nimport { DEFAULT_CHAIN_ID, getDefaultRpcUrl, SUPPORTED_CHAIN_IDS } from \"../constants\";\nimport {\n GeminiStorage,\n type IStorage,\n STORAGE_CALL_BATCHES_KEY,\n STORAGE_ETH_ACCOUNTS_KEY,\n STORAGE_ETH_ACTIVE_CHAIN_KEY,\n} from \"../storage\";\nimport {\n type CallBatchMetadata,\n type Chain,\n type ConnectResponse,\n type GeminiProviderConfig,\n GeminiSdkEvent,\n type GeminiSdkMessage,\n type GeminiSdkMessageResponse,\n type GeminiSdkSendTransaction,\n type GeminiSdkSignMessage,\n type GeminiSdkSignTypedData,\n type GetCallsStatusResponse,\n type SendCallsParams,\n type SendCallsResponse,\n type SendTransactionResponse,\n type SignMessageResponse,\n type SignTypedDataResponse,\n type SwitchChainResponse,\n type WalletCapabilities,\n} from \"../types\";\nimport { hexStringFromNumber } from \"../utils\";\n\nexport function isChainSupportedByGeminiSw(chainId: number): boolean {\n return SUPPORTED_CHAIN_IDS.includes(chainId as (typeof SUPPORTED_CHAIN_IDS)[number]);\n}\n\nexport class GeminiWallet {\n private readonly communicator: Communicator;\n private readonly storage: IStorage;\n private initPromise: Promise<void>;\n public accounts: Address[] = [];\n public chain: Chain = { id: DEFAULT_CHAIN_ID };\n\n constructor({ appMetadata, chain, onDisconnectCallback, storage }: Readonly<GeminiProviderConfig>) {\n this.communicator = new Communicator({\n appMetadata,\n onDisconnectCallback,\n });\n // Use provided storage or create default GeminiStorage for web\n this.storage = storage || new GeminiStorage();\n\n // Initialize storage data - use provided chain