@gemini-wallet/core
Advanced tools
+250
-89
@@ -35,2 +35,3 @@ "use strict"; | ||
| SDK_VERSION: () => SDK_VERSION, | ||
| STORAGE_CALL_BATCHES_KEY: () => STORAGE_CALL_BATCHES_KEY, | ||
| STORAGE_ETH_ACCOUNTS_KEY: () => STORAGE_ETH_ACCOUNTS_KEY, | ||
@@ -73,3 +74,3 @@ STORAGE_ETH_ACTIVE_CHAIN_KEY: () => STORAGE_ETH_ACTIVE_CHAIN_KEY, | ||
| name: "@gemini-wallet/core", | ||
| version: "0.3.1", | ||
| version: "0.3.2", | ||
| description: "Core SDK for Gemini Wallet integration with popup communication", | ||
@@ -118,21 +119,17 @@ main: "./dist/index.cjs", | ||
| "@eslint/eslintrc": "3.3.1", | ||
| "@eslint/js": "9.33.0", | ||
| "@next/eslint-plugin-next": "15.4.7", | ||
| "@eslint/js": "9.38.0", | ||
| "@types/node": "22.13.0", | ||
| "dotenv-cli": "10.0.0", | ||
| "esbuild-plugin-replace": "1.4.0", | ||
| eslint: "9.33.0", | ||
| eslint: "9.38.0", | ||
| "eslint-config-prettier": "10.1.8", | ||
| "eslint-config-turbo": "2.5.6", | ||
| "eslint-plugin-import": "2.32.0", | ||
| "eslint-plugin-jsx-a11y": "6.10.2", | ||
| "eslint-plugin-only-warn": "1.1.0", | ||
| "eslint-plugin-prettier": "5.5.4", | ||
| "eslint-plugin-react": "7.37.5", | ||
| "eslint-plugin-react-hooks": "5.2.0", | ||
| "eslint-plugin-simple-import-sort": "12.1.1", | ||
| "eslint-plugin-sort-keys-fix": "1.1.2", | ||
| globals: "16.3.0", | ||
| globals: "16.4.0", | ||
| prettier: "3.6.2", | ||
| tsup: "8.4.0", | ||
| tsup: "8.5.0", | ||
| typescript: "5.5.3", | ||
@@ -158,3 +155,3 @@ "typescript-eslint": "8.40.0", | ||
| var DEFAULT_BACKEND_URL = "https://keys.gemini.com"; | ||
| var SDK_BACKEND_URL = typeof process !== "undefined" && "undefined" || DEFAULT_BACKEND_URL; | ||
| var SDK_BACKEND_URL = DEFAULT_BACKEND_URL; | ||
| var ENS_API_URL = "https://horizon-api.gemini.com/api/ens"; | ||
@@ -177,6 +174,3 @@ var SDK_VERSION = package_default.version; | ||
| }; | ||
| var SUPPORTED_CHAIN_IDS = [ | ||
| ...Object.values(MAINNET_CHAIN_IDS), | ||
| ...Object.values(TESTNET_CHAIN_IDS) | ||
| ]; | ||
| var SUPPORTED_CHAIN_IDS = [...Object.values(MAINNET_CHAIN_IDS), ...Object.values(TESTNET_CHAIN_IDS)]; | ||
| function getDefaultRpcUrl(chainId) { | ||
@@ -214,2 +208,6 @@ const chainMap = { | ||
| GeminiSdkEvent2["SDK_CURRENT_ACCOUNT"] = "SDK_CURRENT_ACCOUNT"; | ||
| GeminiSdkEvent2["SDK_SEND_BATCH_CALLS"] = "SDK_SEND_BATCH_CALLS"; | ||
| GeminiSdkEvent2["SDK_GET_CAPABILITIES"] = "SDK_GET_CAPABILITIES"; | ||
| GeminiSdkEvent2["SDK_GET_CALLS_STATUS"] = "SDK_GET_CALLS_STATUS"; | ||
| GeminiSdkEvent2["SDK_SHOW_CALLS_STATUS"] = "SDK_SHOW_CALLS_STATUS"; | ||
| return GeminiSdkEvent2; | ||
@@ -296,5 +294,3 @@ })(GeminiSdkEvent || {}); | ||
| if (!publicKey.startsWith("0x") || publicKey.length !== 130) { | ||
| throw new Error( | ||
| "Invalid public key: must be 64-byte hex string (0x + 128 chars)" | ||
| ); | ||
| throw new Error("Invalid public key: must be 64-byte hex string (0x + 128 chars)"); | ||
| } | ||
@@ -308,5 +304,3 @@ const pubKeyX = `0x${publicKey.slice(2, 66)}`; | ||
| if (!validateWebAuthnKey(webAuthnData)) { | ||
| throw new Error( | ||
| "Invalid WebAuthn key: coordinates are not on secp256r1 curve" | ||
| ); | ||
| throw new Error("Invalid WebAuthn key: coordinates are not on secp256r1 curve"); | ||
| } | ||
@@ -405,12 +399,4 @@ const authenticatorIdHash = generateAuthenticatorIdHash(credentialId); | ||
| }); | ||
| const initData = (0, import_viem.encodeAbiParameters)( | ||
| [{ type: "address" }, { type: "bytes" }], | ||
| [bootstrapper, bootstrapCall] | ||
| ); | ||
| return predictProxyAddress( | ||
| accountImplementation, | ||
| salt, | ||
| initData, | ||
| factoryAddress | ||
| ); | ||
| const initData = (0, import_viem.encodeAbiParameters)([{ type: "address" }, { type: "bytes" }], [bootstrapper, bootstrapCall]); | ||
| return predictProxyAddress(accountImplementation, salt, initData, factoryAddress); | ||
| } | ||
@@ -434,5 +420,3 @@ function predictProxyAddress(implementation, salt, initData, deployer) { | ||
| const nexusProxyCreationCode = "0x60806040526102c8803803806100148161018c565b92833981016040828203126101885781516001600160a01b03811692909190838303610188576020810151906001600160401b03821161018857019281601f8501121561018857835161006e610069826101c5565b61018c565b9481865260208601936020838301011161018857815f926020809301865e8601015260017f90b772c2cb8a51aa7a8a65fc23543c6d022d5b3f8e2b92eed79fba7eef8293005d823b15610176577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc80546001600160a01b031916821790557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b5f80a282511561015e575f8091610146945190845af43d15610156573d91610137610069846101c5565b9283523d5f602085013e6101e0565b505b6040516089908161023f8239f35b6060916101e0565b50505034156101485763b398979f60e01b5f5260045ffd5b634c9c8ce360e01b5f5260045260245ffd5b5f80fd5b6040519190601f01601f191682016001600160401b038111838210176101b157604052565b634e487b7160e01b5f52604160045260245ffd5b6001600160401b0381116101b157601f01601f191660200190565b9061020457508051156101f557805190602001fd5b63d6bda27560e01b5f5260045ffd5b81511580610235575b610215575090565b639996b31560e01b5f9081526001600160a01b0391909116600452602490fd5b50803b1561020d56fe608060405236156051577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545f9081906001600160a01b0316368280378136915af43d5f803e15604d573d5ff35b3d5ffd5b00fea264697066735822122041b5f70a351952142223f22504ca7b4e6d975f3a302d114ff820442fcf815ac264736f6c634300081b0033"; | ||
| const initCodeHash = (0, import_viem.keccak256)( | ||
| (0, import_viem.encodePacked)(["bytes", "bytes"], [nexusProxyCreationCode, constructorArgs]) | ||
| ); | ||
| const initCodeHash = (0, import_viem.keccak256)((0, import_viem.encodePacked)(["bytes", "bytes"], [nexusProxyCreationCode, constructorArgs])); | ||
| return (0, import_viem.getCreate2Address)({ | ||
@@ -450,5 +434,3 @@ bytecodeHash: initCodeHash, | ||
| if (!response.ok) { | ||
| throw new Error( | ||
| `ENS API request failed: ${response.status} ${response.statusText}` | ||
| ); | ||
| throw new Error(`ENS API request failed: ${response.status} ${response.statusText}`); | ||
| } | ||
@@ -477,7 +459,3 @@ const data = await response.json(); | ||
| const popupId = `wallet_${window?.crypto?.randomUUID()}`; | ||
| const popup = window.open( | ||
| url, | ||
| popupId, | ||
| `width=${POPUP_WIDTH2}, height=${POPUP_HEIGHT2}, left=${left}, top=${top}` | ||
| ); | ||
| const popup = window.open(url, popupId, `width=${POPUP_WIDTH2}, height=${POPUP_HEIGHT2}, left=${left}, top=${top}`); | ||
| popup?.focus(); | ||
@@ -500,7 +478,3 @@ if (!popup) { | ||
| }; | ||
| var safeJsonStringify = (obj) => JSON.stringify( | ||
| obj, | ||
| (_, value) => typeof value === "bigint" ? value.toString() + "n" : value, | ||
| 2 | ||
| ); | ||
| var safeJsonStringify = (obj) => JSON.stringify(obj, (_, value) => typeof value === "bigint" ? value.toString() + "n" : value, 2); | ||
@@ -519,5 +493,3 @@ // src/communicator.ts | ||
| this.postRequestAndWaitForResponse = async (request) => { | ||
| const responsePromise = this.onMessage( | ||
| ({ requestId }) => requestId === request.requestId | ||
| ); | ||
| const responsePromise = this.onMessage(({ requestId }) => requestId === request.requestId); | ||
| this.postMessage(request); | ||
@@ -559,9 +531,5 @@ return await responsePromise; | ||
| this.popup = openPopup(this.url); | ||
| this.onMessage( | ||
| ({ event }) => event === "POPUP_UNLOADED" /* POPUP_UNLOADED */ | ||
| ).then(this.onRequestCancelled).catch(() => { | ||
| this.onMessage(({ event }) => event === "POPUP_UNLOADED" /* POPUP_UNLOADED */).then(this.onRequestCancelled).catch(() => { | ||
| }); | ||
| this.onMessage( | ||
| ({ event }) => event === "SDK_DISCONNECT" /* SDK_DISCONNECT */ | ||
| ).then(() => { | ||
| this.onMessage(({ event }) => event === "SDK_DISCONNECT" /* SDK_DISCONNECT */).then(() => { | ||
| this.onDisconnectCallback?.(); | ||
@@ -602,6 +570,3 @@ this.onRequestCancelled(); | ||
| var GeminiStorage = class { | ||
| constructor({ | ||
| scope = "@gemini", | ||
| module: module2 = "wallet" | ||
| } = {}) { | ||
| constructor({ scope = "@gemini", module: module2 = "wallet" } = {}) { | ||
| this.scope = scope; | ||
@@ -673,16 +638,10 @@ this.module = module2; | ||
| var STORAGE_WC_REQUESTS_KEY = "wc-requests"; | ||
| var STORAGE_CALL_BATCHES_KEY = "call-batches"; | ||
| // src/wallets/wallet.ts | ||
| function isChainSupportedByGeminiSw(chainId) { | ||
| return SUPPORTED_CHAIN_IDS.includes( | ||
| chainId | ||
| ); | ||
| return SUPPORTED_CHAIN_IDS.includes(chainId); | ||
| } | ||
| var GeminiWallet = class { | ||
| constructor({ | ||
| appMetadata, | ||
| chain, | ||
| onDisconnectCallback, | ||
| storage | ||
| }) { | ||
| constructor({ appMetadata, chain, onDisconnectCallback, storage }) { | ||
| this.accounts = []; | ||
@@ -709,10 +668,4 @@ this.chain = { id: DEFAULT_CHAIN_ID }; | ||
| const [storedChain, storedAccounts] = await Promise.all([ | ||
| this.storage.loadObject( | ||
| STORAGE_ETH_ACTIVE_CHAIN_KEY, | ||
| fallbackChain | ||
| ), | ||
| this.storage.loadObject( | ||
| STORAGE_ETH_ACCOUNTS_KEY, | ||
| this.accounts | ||
| ) | ||
| this.storage.loadObject(STORAGE_ETH_ACTIVE_CHAIN_KEY, fallbackChain), | ||
| this.storage.loadObject(STORAGE_ETH_ACCOUNTS_KEY, this.accounts) | ||
| ]); | ||
@@ -772,5 +725,3 @@ this.chain = { | ||
| } | ||
| async signData({ | ||
| message | ||
| }) { | ||
| async signData({ message }) { | ||
| await this.ensureInitialized(); | ||
@@ -814,2 +765,159 @@ const response = await this.sendMessageToPopup({ | ||
| } | ||
| // EIP-5792 Wallet Call API Methods | ||
| getCapabilities(requestedChainIds) { | ||
| const capabilities = {}; | ||
| const chainIds = requestedChainIds?.map((id) => parseInt(id, 16)) || [this.chain.id]; | ||
| for (const chainId of chainIds) { | ||
| const chainIdHex = hexStringFromNumber(chainId); | ||
| capabilities[chainIdHex] = { | ||
| atomic: { | ||
| status: "supported" | ||
| // Smart accounts support atomic batch execution | ||
| }, | ||
| paymasterService: { | ||
| supported: true | ||
| } | ||
| }; | ||
| } | ||
| return capabilities; | ||
| } | ||
| async sendCalls(params) { | ||
| await this.ensureInitialized(); | ||
| const batchId = window?.crypto?.randomUUID() || `batch-${Date.now()}-${Math.random()}`; | ||
| const requestedChainId = parseInt(params.chainId, 16); | ||
| if (requestedChainId !== this.chain.id) { | ||
| throw new Error(`Chain mismatch. Expected ${this.chain.id}, got ${requestedChainId}`); | ||
| } | ||
| if (!params.calls || params.calls.length === 0) { | ||
| throw new Error("No calls provided"); | ||
| } | ||
| const batchMetadata = { | ||
| calls: params.calls, | ||
| capabilities: params.capabilities, | ||
| chainId: params.chainId, | ||
| from: params.from, | ||
| id: batchId, | ||
| status: "pending", | ||
| timestamp: Date.now() | ||
| }; | ||
| const batches = await this.storage.loadObject(STORAGE_CALL_BATCHES_KEY, {}); | ||
| batches[batchId] = batchMetadata; | ||
| await this.storage.storeObject(STORAGE_CALL_BATCHES_KEY, batches); | ||
| try { | ||
| const response = await this.sendMessageToPopup({ | ||
| chainId: this.chain.id, | ||
| data: { | ||
| calls: params.calls | ||
| }, | ||
| event: "SDK_SEND_BATCH_CALLS" /* SDK_SEND_BATCH_CALLS */, | ||
| origin: window.location.origin | ||
| }); | ||
| if (response.data.error) { | ||
| throw new Error(response.data.error); | ||
| } | ||
| batchMetadata.transactionHash = response.data.hash; | ||
| batchMetadata.status = "pending"; | ||
| batches[batchId] = batchMetadata; | ||
| await this.storage.storeObject(STORAGE_CALL_BATCHES_KEY, batches); | ||
| return { | ||
| capabilities: { | ||
| caip345: { | ||
| caip2: `eip155:${requestedChainId}`, | ||
| transactionHashes: [response.data.hash] | ||
| } | ||
| }, | ||
| id: batchId | ||
| }; | ||
| } catch (error) { | ||
| batchMetadata.status = "failed"; | ||
| batches[batchId] = batchMetadata; | ||
| await this.storage.storeObject(STORAGE_CALL_BATCHES_KEY, batches); | ||
| throw error; | ||
| } | ||
| } | ||
| async getCallsStatus(batchId) { | ||
| await this.ensureInitialized(); | ||
| const batches = await this.storage.loadObject(STORAGE_CALL_BATCHES_KEY, {}); | ||
| const batch = batches[batchId]; | ||
| if (!batch) { | ||
| throw new Error(`Unknown bundle ID: ${batchId}`); | ||
| } | ||
| if (batch.transactionHash && this.chain.rpcUrl) { | ||
| try { | ||
| const response = await fetch(this.chain.rpcUrl, { | ||
| body: JSON.stringify({ | ||
| id: 1, | ||
| jsonrpc: "2.0", | ||
| method: "eth_getTransactionReceipt", | ||
| params: [batch.transactionHash] | ||
| }), | ||
| headers: { "Content-Type": "application/json" }, | ||
| method: "POST" | ||
| }); | ||
| const json = await response.json(); | ||
| const receipt = json.result; | ||
| if (receipt) { | ||
| const receiptStatus = receipt.status === "0x1" ? "confirmed" : "reverted"; | ||
| batch.status = receiptStatus; | ||
| batches[batchId] = batch; | ||
| await this.storage.storeObject(STORAGE_CALL_BATCHES_KEY, batches); | ||
| return { | ||
| atomic: true, | ||
| chainId: batch.chainId, | ||
| id: batchId, | ||
| receipts: [ | ||
| { | ||
| blockHash: receipt.blockHash, | ||
| blockNumber: receipt.blockNumber, | ||
| gasUsed: receipt.gasUsed, | ||
| logs: receipt.logs.map((log) => ({ | ||
| address: log.address, | ||
| data: log.data, | ||
| topics: log.topics | ||
| })), | ||
| status: receiptStatus === "confirmed" ? "success" : "reverted", | ||
| transactionHash: receipt.transactionHash | ||
| } | ||
| ], | ||
| status: receiptStatus === "confirmed" ? 200 : 500, | ||
| version: "2.0.0" | ||
| }; | ||
| } | ||
| } catch (error) { | ||
| console.error("Failed to fetch transaction receipt:", error); | ||
| } | ||
| } | ||
| let statusCode; | ||
| switch (batch.status) { | ||
| case "pending": | ||
| statusCode = 100; | ||
| break; | ||
| case "confirmed": | ||
| statusCode = 200; | ||
| break; | ||
| case "failed": | ||
| statusCode = 400; | ||
| break; | ||
| case "reverted": | ||
| statusCode = 500; | ||
| break; | ||
| default: | ||
| statusCode = 100; | ||
| } | ||
| return { | ||
| atomic: true, | ||
| chainId: batch.chainId, | ||
| id: batchId, | ||
| status: statusCode, | ||
| version: "2.0.0" | ||
| }; | ||
| } | ||
| async showCallsStatus(batchId) { | ||
| await this.ensureInitialized(); | ||
| const batches = await this.storage.loadObject(STORAGE_CALL_BATCHES_KEY, {}); | ||
| const batch = batches[batchId]; | ||
| if (!batch) { | ||
| throw new Error(`Unknown bundle ID: ${batchId}`); | ||
| } | ||
| } | ||
| sendMessageToPopup(request) { | ||
@@ -982,5 +1090,3 @@ return this.communicator.postRequestAndWaitForResponse({ | ||
| requestParams = args.params; | ||
| const signedTypedDataParams = JSON.parse( | ||
| requestParams[1] | ||
| ); | ||
| const signedTypedDataParams = JSON.parse(requestParams[1]); | ||
| response = await this.wallet.signTypedData({ | ||
@@ -1000,2 +1106,24 @@ account: requestParams[0], | ||
| } | ||
| // EIP-5792 Wallet Call API | ||
| case "wallet_getCapabilities": { | ||
| const capabilityParams = Array.isArray(args.params) ? args.params : void 0; | ||
| response = this.getCapabilities(capabilityParams); | ||
| break; | ||
| } | ||
| case "wallet_sendCalls": { | ||
| requestParams = args.params; | ||
| response = await this.sendCalls(requestParams[0]); | ||
| break; | ||
| } | ||
| case "wallet_getCallsStatus": { | ||
| requestParams = args.params; | ||
| response = await this.getCallsStatus(requestParams[0]); | ||
| break; | ||
| } | ||
| case "wallet_showCallsStatus": { | ||
| requestParams = args.params; | ||
| await this.showCallsStatus(requestParams[0]); | ||
| response = null; | ||
| break; | ||
| } | ||
| // TODO: not yet implemented or unclear if we support | ||
@@ -1008,6 +1136,2 @@ case "eth_ecRecover": | ||
| case "wallet_watchAsset": | ||
| case "wallet_sendCalls": | ||
| case "wallet_getCallsStatus": | ||
| case "wallet_getCapabilities": | ||
| case "wallet_showCallsStatus": | ||
| case "wallet_grantPermissions": | ||
@@ -1023,5 +1147,3 @@ throw import_rpc_errors4.rpcErrors.methodNotSupported("Not yet implemented."); | ||
| if (!this.wallet.chain.rpcUrl) | ||
| throw import_rpc_errors4.rpcErrors.internal( | ||
| `RPC URL missing for current chain (${this.wallet.chain.id})` | ||
| ); | ||
| throw import_rpc_errors4.rpcErrors.internal(`RPC URL missing for current chain (${this.wallet.chain.id})`); | ||
| return fetchRpcRequest(args, this.wallet.chain.rpcUrl); | ||
@@ -1040,2 +1162,40 @@ } | ||
| } | ||
| // EIP-5792 Implementation Methods - delegating to wallet | ||
| getCapabilities(params) { | ||
| if (!this.wallet) { | ||
| throw import_rpc_errors4.providerErrors.unauthorized(); | ||
| } | ||
| const requestedChainIds = params?.[0]; | ||
| return this.wallet.getCapabilities(requestedChainIds); | ||
| } | ||
| async sendCalls(params) { | ||
| if (!this.wallet) { | ||
| throw import_rpc_errors4.providerErrors.unauthorized(); | ||
| } | ||
| try { | ||
| return await this.wallet.sendCalls(params); | ||
| } catch (error) { | ||
| throw import_rpc_errors4.rpcErrors.transactionRejected(error instanceof Error ? error.message : String(error)); | ||
| } | ||
| } | ||
| async getCallsStatus(batchId) { | ||
| if (!this.wallet) { | ||
| throw import_rpc_errors4.providerErrors.unauthorized(); | ||
| } | ||
| try { | ||
| return await this.wallet.getCallsStatus(batchId); | ||
| } catch (error) { | ||
| throw import_rpc_errors4.rpcErrors.invalidParams(error instanceof Error ? error.message : String(error)); | ||
| } | ||
| } | ||
| async showCallsStatus(batchId) { | ||
| if (!this.wallet) { | ||
| throw import_rpc_errors4.providerErrors.unauthorized(); | ||
| } | ||
| try { | ||
| await this.wallet.showCallsStatus(batchId); | ||
| } catch (error) { | ||
| throw import_rpc_errors4.rpcErrors.invalidParams(error instanceof Error ? error.message : String(error)); | ||
| } | ||
| } | ||
| async disconnect() { | ||
@@ -1067,2 +1227,3 @@ if (this.wallet) { | ||
| SDK_VERSION, | ||
| STORAGE_CALL_BATCHES_KEY, | ||
| STORAGE_ETH_ACCOUNTS_KEY, | ||
@@ -1069,0 +1230,0 @@ STORAGE_ETH_ACTIVE_CHAIN_KEY, |
+103
-8
@@ -51,2 +51,3 @@ import { EventEmitter } from 'eventemitter3'; | ||
| declare const STORAGE_WC_REQUESTS_KEY = "wc-requests"; | ||
| declare const STORAGE_CALL_BATCHES_KEY = "call-batches"; | ||
@@ -64,11 +65,33 @@ declare enum GeminiSdkEvent { | ||
| SDK_OPEN_SETTINGS = "SDK_OPEN_SETTINGS", | ||
| SDK_CURRENT_ACCOUNT = "SDK_CURRENT_ACCOUNT" | ||
| SDK_CURRENT_ACCOUNT = "SDK_CURRENT_ACCOUNT", | ||
| SDK_SEND_BATCH_CALLS = "SDK_SEND_BATCH_CALLS", | ||
| SDK_GET_CAPABILITIES = "SDK_GET_CAPABILITIES", | ||
| SDK_GET_CALLS_STATUS = "SDK_GET_CALLS_STATUS", | ||
| SDK_SHOW_CALLS_STATUS = "SDK_SHOW_CALLS_STATUS" | ||
| } | ||
| interface AppMetadata { | ||
| appName?: string; | ||
| appLogoUrl?: string; | ||
| /** | ||
| * The name of your application | ||
| */ | ||
| name?: string; | ||
| /** | ||
| * The description of your application (optional) | ||
| */ | ||
| description?: string; | ||
| /** | ||
| * URL of your application | ||
| */ | ||
| url?: string; | ||
| icons?: string[]; | ||
| /** | ||
| * URL to your application's icon or logo | ||
| */ | ||
| icon?: string; | ||
| /** | ||
| * @deprecated Use `name` instead | ||
| */ | ||
| appName?: string; | ||
| /** | ||
| * @deprecated Use `icon` instead | ||
| */ | ||
| appLogoUrl?: string; | ||
| } | ||
@@ -173,2 +196,5 @@ interface AppContext { | ||
| } | ||
| interface GeminiSdkSendBatchCalls extends Omit<GeminiSdkMessage, "data"> { | ||
| data: SendCallsParams; | ||
| } | ||
| interface GeminiSdkSwitchChain extends Omit<GeminiSdkMessage, "data"> { | ||
@@ -184,2 +210,63 @@ data: number; | ||
| } | ||
| interface Call { | ||
| to: Address; | ||
| value?: Hex; | ||
| data?: Hex; | ||
| chainId?: Hex; | ||
| } | ||
| interface SendCallsParams { | ||
| version: string; | ||
| chainId: Hex; | ||
| from: Address; | ||
| calls: Call[]; | ||
| capabilities?: Record<string, any>; | ||
| } | ||
| interface WalletCapabilities { | ||
| [chainId: string]: { | ||
| atomic?: { | ||
| status: "supported" | "unsupported"; | ||
| }; | ||
| paymasterService?: { | ||
| supported: boolean; | ||
| }; | ||
| }; | ||
| } | ||
| interface CallBatchMetadata { | ||
| id: string; | ||
| chainId: string; | ||
| from: Address; | ||
| calls: Call[]; | ||
| transactionHash?: Hex; | ||
| status: "pending" | "confirmed" | "failed" | "reverted"; | ||
| timestamp: number; | ||
| capabilities?: Record<string, any>; | ||
| } | ||
| interface GetCallsStatusResponse { | ||
| version: string; | ||
| id: string; | ||
| chainId: Hex; | ||
| status: 100 | 200 | 400 | 500; | ||
| atomic: boolean; | ||
| receipts?: Array<{ | ||
| logs: Array<{ | ||
| address: Address; | ||
| data: Hex; | ||
| topics: Hex[]; | ||
| }>; | ||
| status: "success" | "reverted"; | ||
| blockHash: Hex; | ||
| blockNumber: Hex; | ||
| gasUsed: Hex; | ||
| transactionHash: Hex; | ||
| }>; | ||
| } | ||
| interface SendCallsResponse { | ||
| id: string; | ||
| capabilities?: { | ||
| caip345?: { | ||
| caip2: string; | ||
| transactionHashes: Hex[]; | ||
| }; | ||
| }; | ||
| } | ||
@@ -210,2 +297,6 @@ type CommunicatorConfigParams = { | ||
| openSettings(): Promise<void>; | ||
| private getCapabilities; | ||
| private sendCalls; | ||
| private getCallsStatus; | ||
| private showCallsStatus; | ||
| disconnect(): Promise<void>; | ||
@@ -242,3 +333,3 @@ } | ||
| chain: Chain; | ||
| constructor({ appMetadata, chain, onDisconnectCallback, storage, }: Readonly<GeminiProviderConfig>); | ||
| constructor({ appMetadata, chain, onDisconnectCallback, storage }: Readonly<GeminiProviderConfig>); | ||
| private initializeFromStorage; | ||
@@ -250,5 +341,9 @@ private ensureInitialized; | ||
| sendTransaction(txData: TransactionRequest): Promise<SendTransactionResponse["data"]>; | ||
| signData({ message, }: SignMessageParameters): Promise<SignMessageResponse["data"]>; | ||
| signData({ message }: SignMessageParameters): Promise<SignMessageResponse["data"]>; | ||
| signTypedData({ message, types, primaryType, domain, }: SignTypedDataParameters): Promise<SignTypedDataResponse["data"]>; | ||
| openSettings(): Promise<void>; | ||
| getCapabilities(requestedChainIds?: string[]): WalletCapabilities; | ||
| sendCalls(params: SendCallsParams): Promise<SendCallsResponse>; | ||
| getCallsStatus(batchId: string): Promise<GetCallsStatusResponse>; | ||
| showCallsStatus(batchId: string): Promise<void>; | ||
| private sendMessageToPopup; | ||
@@ -268,3 +363,3 @@ } | ||
| private module; | ||
| constructor({ scope, module, }?: GeminiStorageConfig); | ||
| constructor({ scope, module }?: GeminiStorageConfig); | ||
| private scopedKey; | ||
@@ -357,2 +452,2 @@ storeObject<T>(key: string, item: T): Promise<void>; | ||
| export { type AppContext, type AppMetadata, type CalculateWalletAddressParams, type Chain, Communicator, type ConnectResponse, DEFAULT_CHAIN_ID, type GeminiProviderConfig, type GeminiSdkAppContextMessage, GeminiSdkEvent, type GeminiSdkMessage, type GeminiSdkMessageResponse, type GeminiSdkSendTransaction, type GeminiSdkSignMessage, type GeminiSdkSignTypedData, type GeminiSdkSwitchChain, GeminiStorage, type GeminiStorageConfig, GeminiWallet, GeminiWalletProvider, type IStorage, POPUP_HEIGHT, POPUP_WIDTH, PlatformType, type ProviderEventCallback, ProviderEventEmitter, type ProviderEventMap, type ProviderInterface, type ProviderRpcError, type ReverseEnsResponse, type RpcRequestArgs, SDK_BACKEND_URL, SDK_VERSION, STORAGE_ETH_ACCOUNTS_KEY, STORAGE_ETH_ACTIVE_CHAIN_KEY, STORAGE_PASSKEY_CREDENTIAL_KEY, STORAGE_PRESERVED_PASSKEY_CREDENTIALS_KEY, STORAGE_SETTINGS_KEY, STORAGE_SMART_ACCOUNT_KEY, STORAGE_WC_REQUESTS_KEY, type SendTransactionResponse, type SignMessageResponse, type SignTypedDataResponse, type SwitchChainResponse, type WebAuthnValidatorData, base64ToHex, bufferToBase64URLString, calculateV1Address, calculateWalletAddress, closePopup, convertSendValuesToBigInt, decodeBase64, encodeBase64, fetchRpcRequest, generateAuthenticatorIdHash, hexStringFromNumber, isChainSupportedByGeminiSw, openPopup, reverseResolveEns, safeJsonStringify, utf8StringToBuffer, validateRpcRequestArgs, validateWebAuthnKey }; | ||
| export { type AppContext, type AppMetadata, type CalculateWalletAddressParams, type Call, type CallBatchMetadata, type Chain, Communicator, type ConnectResponse, DEFAULT_CHAIN_ID, type GeminiProviderConfig, type GeminiSdkAppContextMessage, GeminiSdkEvent, type GeminiSdkMessage, type GeminiSdkMessageResponse, type GeminiSdkSendBatchCalls, type GeminiSdkSendTransaction, type GeminiSdkSignMessage, type GeminiSdkSignTypedData, type GeminiSdkSwitchChain, GeminiStorage, type GeminiStorageConfig, GeminiWallet, GeminiWalletProvider, type GetCallsStatusResponse, type IStorage, POPUP_HEIGHT, POPUP_WIDTH, PlatformType, type ProviderEventCallback, ProviderEventEmitter, type ProviderEventMap, type ProviderInterface, type ProviderRpcError, type ReverseEnsResponse, type RpcRequestArgs, SDK_BACKEND_URL, SDK_VERSION, STORAGE_CALL_BATCHES_KEY, STORAGE_ETH_ACCOUNTS_KEY, STORAGE_ETH_ACTIVE_CHAIN_KEY, STORAGE_PASSKEY_CREDENTIAL_KEY, STORAGE_PRESERVED_PASSKEY_CREDENTIALS_KEY, STORAGE_SETTINGS_KEY, STORAGE_SMART_ACCOUNT_KEY, STORAGE_WC_REQUESTS_KEY, type SendCallsParams, type SendCallsResponse, type SendTransactionResponse, type SignMessageResponse, type SignTypedDataResponse, type SwitchChainResponse, type WalletCapabilities, type WebAuthnValidatorData, base64ToHex, bufferToBase64URLString, calculateV1Address, calculateWalletAddress, closePopup, convertSendValuesToBigInt, decodeBase64, encodeBase64, fetchRpcRequest, generateAuthenticatorIdHash, hexStringFromNumber, isChainSupportedByGeminiSw, openPopup, reverseResolveEns, safeJsonStringify, utf8StringToBuffer, validateRpcRequestArgs, validateWebAuthnKey }; |
+103
-8
@@ -51,2 +51,3 @@ import { EventEmitter } from 'eventemitter3'; | ||
| declare const STORAGE_WC_REQUESTS_KEY = "wc-requests"; | ||
| declare const STORAGE_CALL_BATCHES_KEY = "call-batches"; | ||
@@ -64,11 +65,33 @@ declare enum GeminiSdkEvent { | ||
| SDK_OPEN_SETTINGS = "SDK_OPEN_SETTINGS", | ||
| SDK_CURRENT_ACCOUNT = "SDK_CURRENT_ACCOUNT" | ||
| SDK_CURRENT_ACCOUNT = "SDK_CURRENT_ACCOUNT", | ||
| SDK_SEND_BATCH_CALLS = "SDK_SEND_BATCH_CALLS", | ||
| SDK_GET_CAPABILITIES = "SDK_GET_CAPABILITIES", | ||
| SDK_GET_CALLS_STATUS = "SDK_GET_CALLS_STATUS", | ||
| SDK_SHOW_CALLS_STATUS = "SDK_SHOW_CALLS_STATUS" | ||
| } | ||
| interface AppMetadata { | ||
| appName?: string; | ||
| appLogoUrl?: string; | ||
| /** | ||
| * The name of your application | ||
| */ | ||
| name?: string; | ||
| /** | ||
| * The description of your application (optional) | ||
| */ | ||
| description?: string; | ||
| /** | ||
| * URL of your application | ||
| */ | ||
| url?: string; | ||
| icons?: string[]; | ||
| /** | ||
| * URL to your application's icon or logo | ||
| */ | ||
| icon?: string; | ||
| /** | ||
| * @deprecated Use `name` instead | ||
| */ | ||
| appName?: string; | ||
| /** | ||
| * @deprecated Use `icon` instead | ||
| */ | ||
| appLogoUrl?: string; | ||
| } | ||
@@ -173,2 +196,5 @@ interface AppContext { | ||
| } | ||
| interface GeminiSdkSendBatchCalls extends Omit<GeminiSdkMessage, "data"> { | ||
| data: SendCallsParams; | ||
| } | ||
| interface GeminiSdkSwitchChain extends Omit<GeminiSdkMessage, "data"> { | ||
@@ -184,2 +210,63 @@ data: number; | ||
| } | ||
| interface Call { | ||
| to: Address; | ||
| value?: Hex; | ||
| data?: Hex; | ||
| chainId?: Hex; | ||
| } | ||
| interface SendCallsParams { | ||
| version: string; | ||
| chainId: Hex; | ||
| from: Address; | ||
| calls: Call[]; | ||
| capabilities?: Record<string, any>; | ||
| } | ||
| interface WalletCapabilities { | ||
| [chainId: string]: { | ||
| atomic?: { | ||
| status: "supported" | "unsupported"; | ||
| }; | ||
| paymasterService?: { | ||
| supported: boolean; | ||
| }; | ||
| }; | ||
| } | ||
| interface CallBatchMetadata { | ||
| id: string; | ||
| chainId: string; | ||
| from: Address; | ||
| calls: Call[]; | ||
| transactionHash?: Hex; | ||
| status: "pending" | "confirmed" | "failed" | "reverted"; | ||
| timestamp: number; | ||
| capabilities?: Record<string, any>; | ||
| } | ||
| interface GetCallsStatusResponse { | ||
| version: string; | ||
| id: string; | ||
| chainId: Hex; | ||
| status: 100 | 200 | 400 | 500; | ||
| atomic: boolean; | ||
| receipts?: Array<{ | ||
| logs: Array<{ | ||
| address: Address; | ||
| data: Hex; | ||
| topics: Hex[]; | ||
| }>; | ||
| status: "success" | "reverted"; | ||
| blockHash: Hex; | ||
| blockNumber: Hex; | ||
| gasUsed: Hex; | ||
| transactionHash: Hex; | ||
| }>; | ||
| } | ||
| interface SendCallsResponse { | ||
| id: string; | ||
| capabilities?: { | ||
| caip345?: { | ||
| caip2: string; | ||
| transactionHashes: Hex[]; | ||
| }; | ||
| }; | ||
| } | ||
@@ -210,2 +297,6 @@ type CommunicatorConfigParams = { | ||
| openSettings(): Promise<void>; | ||
| private getCapabilities; | ||
| private sendCalls; | ||
| private getCallsStatus; | ||
| private showCallsStatus; | ||
| disconnect(): Promise<void>; | ||
@@ -242,3 +333,3 @@ } | ||
| chain: Chain; | ||
| constructor({ appMetadata, chain, onDisconnectCallback, storage, }: Readonly<GeminiProviderConfig>); | ||
| constructor({ appMetadata, chain, onDisconnectCallback, storage }: Readonly<GeminiProviderConfig>); | ||
| private initializeFromStorage; | ||
@@ -250,5 +341,9 @@ private ensureInitialized; | ||
| sendTransaction(txData: TransactionRequest): Promise<SendTransactionResponse["data"]>; | ||
| signData({ message, }: SignMessageParameters): Promise<SignMessageResponse["data"]>; | ||
| signData({ message }: SignMessageParameters): Promise<SignMessageResponse["data"]>; | ||
| signTypedData({ message, types, primaryType, domain, }: SignTypedDataParameters): Promise<SignTypedDataResponse["data"]>; | ||
| openSettings(): Promise<void>; | ||
| getCapabilities(requestedChainIds?: string[]): WalletCapabilities; | ||
| sendCalls(params: SendCallsParams): Promise<SendCallsResponse>; | ||
| getCallsStatus(batchId: string): Promise<GetCallsStatusResponse>; | ||
| showCallsStatus(batchId: string): Promise<void>; | ||
| private sendMessageToPopup; | ||
@@ -268,3 +363,3 @@ } | ||
| private module; | ||
| constructor({ scope, module, }?: GeminiStorageConfig); | ||
| constructor({ scope, module }?: GeminiStorageConfig); | ||
| private scopedKey; | ||
@@ -357,2 +452,2 @@ storeObject<T>(key: string, item: T): Promise<void>; | ||
| export { type AppContext, type AppMetadata, type CalculateWalletAddressParams, type Chain, Communicator, type ConnectResponse, DEFAULT_CHAIN_ID, type GeminiProviderConfig, type GeminiSdkAppContextMessage, GeminiSdkEvent, type GeminiSdkMessage, type GeminiSdkMessageResponse, type GeminiSdkSendTransaction, type GeminiSdkSignMessage, type GeminiSdkSignTypedData, type GeminiSdkSwitchChain, GeminiStorage, type GeminiStorageConfig, GeminiWallet, GeminiWalletProvider, type IStorage, POPUP_HEIGHT, POPUP_WIDTH, PlatformType, type ProviderEventCallback, ProviderEventEmitter, type ProviderEventMap, type ProviderInterface, type ProviderRpcError, type ReverseEnsResponse, type RpcRequestArgs, SDK_BACKEND_URL, SDK_VERSION, STORAGE_ETH_ACCOUNTS_KEY, STORAGE_ETH_ACTIVE_CHAIN_KEY, STORAGE_PASSKEY_CREDENTIAL_KEY, STORAGE_PRESERVED_PASSKEY_CREDENTIALS_KEY, STORAGE_SETTINGS_KEY, STORAGE_SMART_ACCOUNT_KEY, STORAGE_WC_REQUESTS_KEY, type SendTransactionResponse, type SignMessageResponse, type SignTypedDataResponse, type SwitchChainResponse, type WebAuthnValidatorData, base64ToHex, bufferToBase64URLString, calculateV1Address, calculateWalletAddress, closePopup, convertSendValuesToBigInt, decodeBase64, encodeBase64, fetchRpcRequest, generateAuthenticatorIdHash, hexStringFromNumber, isChainSupportedByGeminiSw, openPopup, reverseResolveEns, safeJsonStringify, utf8StringToBuffer, validateRpcRequestArgs, validateWebAuthnKey }; | ||
| export { type AppContext, type AppMetadata, type CalculateWalletAddressParams, type Call, type CallBatchMetadata, type Chain, Communicator, type ConnectResponse, DEFAULT_CHAIN_ID, type GeminiProviderConfig, type GeminiSdkAppContextMessage, GeminiSdkEvent, type GeminiSdkMessage, type GeminiSdkMessageResponse, type GeminiSdkSendBatchCalls, type GeminiSdkSendTransaction, type GeminiSdkSignMessage, type GeminiSdkSignTypedData, type GeminiSdkSwitchChain, GeminiStorage, type GeminiStorageConfig, GeminiWallet, GeminiWalletProvider, type GetCallsStatusResponse, type IStorage, POPUP_HEIGHT, POPUP_WIDTH, PlatformType, type ProviderEventCallback, ProviderEventEmitter, type ProviderEventMap, type ProviderInterface, type ProviderRpcError, type ReverseEnsResponse, type RpcRequestArgs, SDK_BACKEND_URL, SDK_VERSION, STORAGE_CALL_BATCHES_KEY, STORAGE_ETH_ACCOUNTS_KEY, STORAGE_ETH_ACTIVE_CHAIN_KEY, STORAGE_PASSKEY_CREDENTIAL_KEY, STORAGE_PRESERVED_PASSKEY_CREDENTIALS_KEY, STORAGE_SETTINGS_KEY, STORAGE_SMART_ACCOUNT_KEY, STORAGE_WC_REQUESTS_KEY, type SendCallsParams, type SendCallsResponse, type SendTransactionResponse, type SignMessageResponse, type SignTypedDataResponse, type SwitchChainResponse, type WalletCapabilities, type WebAuthnValidatorData, base64ToHex, bufferToBase64URLString, calculateV1Address, calculateWalletAddress, closePopup, convertSendValuesToBigInt, decodeBase64, encodeBase64, fetchRpcRequest, generateAuthenticatorIdHash, hexStringFromNumber, isChainSupportedByGeminiSw, openPopup, reverseResolveEns, safeJsonStringify, utf8StringToBuffer, validateRpcRequestArgs, validateWebAuthnKey }; |
+250
-95
@@ -21,3 +21,3 @@ // src/communicator.ts | ||
| name: "@gemini-wallet/core", | ||
| version: "0.3.1", | ||
| version: "0.3.2", | ||
| description: "Core SDK for Gemini Wallet integration with popup communication", | ||
@@ -66,21 +66,17 @@ main: "./dist/index.cjs", | ||
| "@eslint/eslintrc": "3.3.1", | ||
| "@eslint/js": "9.33.0", | ||
| "@next/eslint-plugin-next": "15.4.7", | ||
| "@eslint/js": "9.38.0", | ||
| "@types/node": "22.13.0", | ||
| "dotenv-cli": "10.0.0", | ||
| "esbuild-plugin-replace": "1.4.0", | ||
| eslint: "9.33.0", | ||
| eslint: "9.38.0", | ||
| "eslint-config-prettier": "10.1.8", | ||
| "eslint-config-turbo": "2.5.6", | ||
| "eslint-plugin-import": "2.32.0", | ||
| "eslint-plugin-jsx-a11y": "6.10.2", | ||
| "eslint-plugin-only-warn": "1.1.0", | ||
| "eslint-plugin-prettier": "5.5.4", | ||
| "eslint-plugin-react": "7.37.5", | ||
| "eslint-plugin-react-hooks": "5.2.0", | ||
| "eslint-plugin-simple-import-sort": "12.1.1", | ||
| "eslint-plugin-sort-keys-fix": "1.1.2", | ||
| globals: "16.3.0", | ||
| globals: "16.4.0", | ||
| prettier: "3.6.2", | ||
| tsup: "8.4.0", | ||
| tsup: "8.5.0", | ||
| typescript: "5.5.3", | ||
@@ -106,3 +102,3 @@ "typescript-eslint": "8.40.0", | ||
| var DEFAULT_BACKEND_URL = "https://keys.gemini.com"; | ||
| var SDK_BACKEND_URL = typeof process !== "undefined" && "undefined" || DEFAULT_BACKEND_URL; | ||
| var SDK_BACKEND_URL = DEFAULT_BACKEND_URL; | ||
| var ENS_API_URL = "https://horizon-api.gemini.com/api/ens"; | ||
@@ -125,6 +121,3 @@ var SDK_VERSION = package_default.version; | ||
| }; | ||
| var SUPPORTED_CHAIN_IDS = [ | ||
| ...Object.values(MAINNET_CHAIN_IDS), | ||
| ...Object.values(TESTNET_CHAIN_IDS) | ||
| ]; | ||
| var SUPPORTED_CHAIN_IDS = [...Object.values(MAINNET_CHAIN_IDS), ...Object.values(TESTNET_CHAIN_IDS)]; | ||
| function getDefaultRpcUrl(chainId) { | ||
@@ -162,2 +155,6 @@ const chainMap = { | ||
| GeminiSdkEvent2["SDK_CURRENT_ACCOUNT"] = "SDK_CURRENT_ACCOUNT"; | ||
| GeminiSdkEvent2["SDK_SEND_BATCH_CALLS"] = "SDK_SEND_BATCH_CALLS"; | ||
| GeminiSdkEvent2["SDK_GET_CAPABILITIES"] = "SDK_GET_CAPABILITIES"; | ||
| GeminiSdkEvent2["SDK_GET_CALLS_STATUS"] = "SDK_GET_CALLS_STATUS"; | ||
| GeminiSdkEvent2["SDK_SHOW_CALLS_STATUS"] = "SDK_SHOW_CALLS_STATUS"; | ||
| return GeminiSdkEvent2; | ||
@@ -250,5 +247,3 @@ })(GeminiSdkEvent || {}); | ||
| if (!publicKey.startsWith("0x") || publicKey.length !== 130) { | ||
| throw new Error( | ||
| "Invalid public key: must be 64-byte hex string (0x + 128 chars)" | ||
| ); | ||
| throw new Error("Invalid public key: must be 64-byte hex string (0x + 128 chars)"); | ||
| } | ||
@@ -262,5 +257,3 @@ const pubKeyX = `0x${publicKey.slice(2, 66)}`; | ||
| if (!validateWebAuthnKey(webAuthnData)) { | ||
| throw new Error( | ||
| "Invalid WebAuthn key: coordinates are not on secp256r1 curve" | ||
| ); | ||
| throw new Error("Invalid WebAuthn key: coordinates are not on secp256r1 curve"); | ||
| } | ||
@@ -359,12 +352,4 @@ const authenticatorIdHash = generateAuthenticatorIdHash(credentialId); | ||
| }); | ||
| const initData = encodeAbiParameters( | ||
| [{ type: "address" }, { type: "bytes" }], | ||
| [bootstrapper, bootstrapCall] | ||
| ); | ||
| return predictProxyAddress( | ||
| accountImplementation, | ||
| salt, | ||
| initData, | ||
| factoryAddress | ||
| ); | ||
| const initData = encodeAbiParameters([{ type: "address" }, { type: "bytes" }], [bootstrapper, bootstrapCall]); | ||
| return predictProxyAddress(accountImplementation, salt, initData, factoryAddress); | ||
| } | ||
@@ -388,5 +373,3 @@ function predictProxyAddress(implementation, salt, initData, deployer) { | ||
| const nexusProxyCreationCode = "0x60806040526102c8803803806100148161018c565b92833981016040828203126101885781516001600160a01b03811692909190838303610188576020810151906001600160401b03821161018857019281601f8501121561018857835161006e610069826101c5565b61018c565b9481865260208601936020838301011161018857815f926020809301865e8601015260017f90b772c2cb8a51aa7a8a65fc23543c6d022d5b3f8e2b92eed79fba7eef8293005d823b15610176577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc80546001600160a01b031916821790557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b5f80a282511561015e575f8091610146945190845af43d15610156573d91610137610069846101c5565b9283523d5f602085013e6101e0565b505b6040516089908161023f8239f35b6060916101e0565b50505034156101485763b398979f60e01b5f5260045ffd5b634c9c8ce360e01b5f5260045260245ffd5b5f80fd5b6040519190601f01601f191682016001600160401b038111838210176101b157604052565b634e487b7160e01b5f52604160045260245ffd5b6001600160401b0381116101b157601f01601f191660200190565b9061020457508051156101f557805190602001fd5b63d6bda27560e01b5f5260045ffd5b81511580610235575b610215575090565b639996b31560e01b5f9081526001600160a01b0391909116600452602490fd5b50803b1561020d56fe608060405236156051577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545f9081906001600160a01b0316368280378136915af43d5f803e15604d573d5ff35b3d5ffd5b00fea264697066735822122041b5f70a351952142223f22504ca7b4e6d975f3a302d114ff820442fcf815ac264736f6c634300081b0033"; | ||
| const initCodeHash = keccak256( | ||
| encodePacked(["bytes", "bytes"], [nexusProxyCreationCode, constructorArgs]) | ||
| ); | ||
| const initCodeHash = keccak256(encodePacked(["bytes", "bytes"], [nexusProxyCreationCode, constructorArgs])); | ||
| return getCreate2Address({ | ||
@@ -404,5 +387,3 @@ bytecodeHash: initCodeHash, | ||
| if (!response.ok) { | ||
| throw new Error( | ||
| `ENS API request failed: ${response.status} ${response.statusText}` | ||
| ); | ||
| throw new Error(`ENS API request failed: ${response.status} ${response.statusText}`); | ||
| } | ||
@@ -431,7 +412,3 @@ const data = await response.json(); | ||
| const popupId = `wallet_${window?.crypto?.randomUUID()}`; | ||
| const popup = window.open( | ||
| url, | ||
| popupId, | ||
| `width=${POPUP_WIDTH2}, height=${POPUP_HEIGHT2}, left=${left}, top=${top}` | ||
| ); | ||
| const popup = window.open(url, popupId, `width=${POPUP_WIDTH2}, height=${POPUP_HEIGHT2}, left=${left}, top=${top}`); | ||
| popup?.focus(); | ||
@@ -454,7 +431,3 @@ if (!popup) { | ||
| }; | ||
| var safeJsonStringify = (obj) => JSON.stringify( | ||
| obj, | ||
| (_, value) => typeof value === "bigint" ? value.toString() + "n" : value, | ||
| 2 | ||
| ); | ||
| var safeJsonStringify = (obj) => JSON.stringify(obj, (_, value) => typeof value === "bigint" ? value.toString() + "n" : value, 2); | ||
@@ -473,5 +446,3 @@ // src/communicator.ts | ||
| this.postRequestAndWaitForResponse = async (request) => { | ||
| const responsePromise = this.onMessage( | ||
| ({ requestId }) => requestId === request.requestId | ||
| ); | ||
| const responsePromise = this.onMessage(({ requestId }) => requestId === request.requestId); | ||
| this.postMessage(request); | ||
@@ -513,9 +484,5 @@ return await responsePromise; | ||
| this.popup = openPopup(this.url); | ||
| this.onMessage( | ||
| ({ event }) => event === "POPUP_UNLOADED" /* POPUP_UNLOADED */ | ||
| ).then(this.onRequestCancelled).catch(() => { | ||
| this.onMessage(({ event }) => event === "POPUP_UNLOADED" /* POPUP_UNLOADED */).then(this.onRequestCancelled).catch(() => { | ||
| }); | ||
| this.onMessage( | ||
| ({ event }) => event === "SDK_DISCONNECT" /* SDK_DISCONNECT */ | ||
| ).then(() => { | ||
| this.onMessage(({ event }) => event === "SDK_DISCONNECT" /* SDK_DISCONNECT */).then(() => { | ||
| this.onDisconnectCallback?.(); | ||
@@ -551,8 +518,3 @@ this.onRequestCancelled(); | ||
| // src/provider/provider.ts | ||
| import { | ||
| errorCodes, | ||
| providerErrors as providerErrors2, | ||
| rpcErrors as rpcErrors4, | ||
| serializeError | ||
| } from "@metamask/rpc-errors"; | ||
| import { errorCodes, providerErrors as providerErrors2, rpcErrors as rpcErrors4, serializeError } from "@metamask/rpc-errors"; | ||
@@ -562,6 +524,3 @@ // src/storage/storage.ts | ||
| var GeminiStorage = class { | ||
| constructor({ | ||
| scope = "@gemini", | ||
| module = "wallet" | ||
| } = {}) { | ||
| constructor({ scope = "@gemini", module = "wallet" } = {}) { | ||
| this.scope = scope; | ||
@@ -633,16 +592,10 @@ this.module = module; | ||
| var STORAGE_WC_REQUESTS_KEY = "wc-requests"; | ||
| var STORAGE_CALL_BATCHES_KEY = "call-batches"; | ||
| // src/wallets/wallet.ts | ||
| function isChainSupportedByGeminiSw(chainId) { | ||
| return SUPPORTED_CHAIN_IDS.includes( | ||
| chainId | ||
| ); | ||
| return SUPPORTED_CHAIN_IDS.includes(chainId); | ||
| } | ||
| var GeminiWallet = class { | ||
| constructor({ | ||
| appMetadata, | ||
| chain, | ||
| onDisconnectCallback, | ||
| storage | ||
| }) { | ||
| constructor({ appMetadata, chain, onDisconnectCallback, storage }) { | ||
| this.accounts = []; | ||
@@ -669,10 +622,4 @@ this.chain = { id: DEFAULT_CHAIN_ID }; | ||
| const [storedChain, storedAccounts] = await Promise.all([ | ||
| this.storage.loadObject( | ||
| STORAGE_ETH_ACTIVE_CHAIN_KEY, | ||
| fallbackChain | ||
| ), | ||
| this.storage.loadObject( | ||
| STORAGE_ETH_ACCOUNTS_KEY, | ||
| this.accounts | ||
| ) | ||
| this.storage.loadObject(STORAGE_ETH_ACTIVE_CHAIN_KEY, fallbackChain), | ||
| this.storage.loadObject(STORAGE_ETH_ACCOUNTS_KEY, this.accounts) | ||
| ]); | ||
@@ -732,5 +679,3 @@ this.chain = { | ||
| } | ||
| async signData({ | ||
| message | ||
| }) { | ||
| async signData({ message }) { | ||
| await this.ensureInitialized(); | ||
@@ -774,2 +719,159 @@ const response = await this.sendMessageToPopup({ | ||
| } | ||
| // EIP-5792 Wallet Call API Methods | ||
| getCapabilities(requestedChainIds) { | ||
| const capabilities = {}; | ||
| const chainIds = requestedChainIds?.map((id) => parseInt(id, 16)) || [this.chain.id]; | ||
| for (const chainId of chainIds) { | ||
| const chainIdHex = hexStringFromNumber(chainId); | ||
| capabilities[chainIdHex] = { | ||
| atomic: { | ||
| status: "supported" | ||
| // Smart accounts support atomic batch execution | ||
| }, | ||
| paymasterService: { | ||
| supported: true | ||
| } | ||
| }; | ||
| } | ||
| return capabilities; | ||
| } | ||
| async sendCalls(params) { | ||
| await this.ensureInitialized(); | ||
| const batchId = window?.crypto?.randomUUID() || `batch-${Date.now()}-${Math.random()}`; | ||
| const requestedChainId = parseInt(params.chainId, 16); | ||
| if (requestedChainId !== this.chain.id) { | ||
| throw new Error(`Chain mismatch. Expected ${this.chain.id}, got ${requestedChainId}`); | ||
| } | ||
| if (!params.calls || params.calls.length === 0) { | ||
| throw new Error("No calls provided"); | ||
| } | ||
| const batchMetadata = { | ||
| calls: params.calls, | ||
| capabilities: params.capabilities, | ||
| chainId: params.chainId, | ||
| from: params.from, | ||
| id: batchId, | ||
| status: "pending", | ||
| timestamp: Date.now() | ||
| }; | ||
| const batches = await this.storage.loadObject(STORAGE_CALL_BATCHES_KEY, {}); | ||
| batches[batchId] = batchMetadata; | ||
| await this.storage.storeObject(STORAGE_CALL_BATCHES_KEY, batches); | ||
| try { | ||
| const response = await this.sendMessageToPopup({ | ||
| chainId: this.chain.id, | ||
| data: { | ||
| calls: params.calls | ||
| }, | ||
| event: "SDK_SEND_BATCH_CALLS" /* SDK_SEND_BATCH_CALLS */, | ||
| origin: window.location.origin | ||
| }); | ||
| if (response.data.error) { | ||
| throw new Error(response.data.error); | ||
| } | ||
| batchMetadata.transactionHash = response.data.hash; | ||
| batchMetadata.status = "pending"; | ||
| batches[batchId] = batchMetadata; | ||
| await this.storage.storeObject(STORAGE_CALL_BATCHES_KEY, batches); | ||
| return { | ||
| capabilities: { | ||
| caip345: { | ||
| caip2: `eip155:${requestedChainId}`, | ||
| transactionHashes: [response.data.hash] | ||
| } | ||
| }, | ||
| id: batchId | ||
| }; | ||
| } catch (error) { | ||
| batchMetadata.status = "failed"; | ||
| batches[batchId] = batchMetadata; | ||
| await this.storage.storeObject(STORAGE_CALL_BATCHES_KEY, batches); | ||
| throw error; | ||
| } | ||
| } | ||
| async getCallsStatus(batchId) { | ||
| await this.ensureInitialized(); | ||
| const batches = await this.storage.loadObject(STORAGE_CALL_BATCHES_KEY, {}); | ||
| const batch = batches[batchId]; | ||
| if (!batch) { | ||
| throw new Error(`Unknown bundle ID: ${batchId}`); | ||
| } | ||
| if (batch.transactionHash && this.chain.rpcUrl) { | ||
| try { | ||
| const response = await fetch(this.chain.rpcUrl, { | ||
| body: JSON.stringify({ | ||
| id: 1, | ||
| jsonrpc: "2.0", | ||
| method: "eth_getTransactionReceipt", | ||
| params: [batch.transactionHash] | ||
| }), | ||
| headers: { "Content-Type": "application/json" }, | ||
| method: "POST" | ||
| }); | ||
| const json = await response.json(); | ||
| const receipt = json.result; | ||
| if (receipt) { | ||
| const receiptStatus = receipt.status === "0x1" ? "confirmed" : "reverted"; | ||
| batch.status = receiptStatus; | ||
| batches[batchId] = batch; | ||
| await this.storage.storeObject(STORAGE_CALL_BATCHES_KEY, batches); | ||
| return { | ||
| atomic: true, | ||
| chainId: batch.chainId, | ||
| id: batchId, | ||
| receipts: [ | ||
| { | ||
| blockHash: receipt.blockHash, | ||
| blockNumber: receipt.blockNumber, | ||
| gasUsed: receipt.gasUsed, | ||
| logs: receipt.logs.map((log) => ({ | ||
| address: log.address, | ||
| data: log.data, | ||
| topics: log.topics | ||
| })), | ||
| status: receiptStatus === "confirmed" ? "success" : "reverted", | ||
| transactionHash: receipt.transactionHash | ||
| } | ||
| ], | ||
| status: receiptStatus === "confirmed" ? 200 : 500, | ||
| version: "2.0.0" | ||
| }; | ||
| } | ||
| } catch (error) { | ||
| console.error("Failed to fetch transaction receipt:", error); | ||
| } | ||
| } | ||
| let statusCode; | ||
| switch (batch.status) { | ||
| case "pending": | ||
| statusCode = 100; | ||
| break; | ||
| case "confirmed": | ||
| statusCode = 200; | ||
| break; | ||
| case "failed": | ||
| statusCode = 400; | ||
| break; | ||
| case "reverted": | ||
| statusCode = 500; | ||
| break; | ||
| default: | ||
| statusCode = 100; | ||
| } | ||
| return { | ||
| atomic: true, | ||
| chainId: batch.chainId, | ||
| id: batchId, | ||
| status: statusCode, | ||
| version: "2.0.0" | ||
| }; | ||
| } | ||
| async showCallsStatus(batchId) { | ||
| await this.ensureInitialized(); | ||
| const batches = await this.storage.loadObject(STORAGE_CALL_BATCHES_KEY, {}); | ||
| const batch = batches[batchId]; | ||
| if (!batch) { | ||
| throw new Error(`Unknown bundle ID: ${batchId}`); | ||
| } | ||
| } | ||
| sendMessageToPopup(request) { | ||
@@ -942,5 +1044,3 @@ return this.communicator.postRequestAndWaitForResponse({ | ||
| requestParams = args.params; | ||
| const signedTypedDataParams = JSON.parse( | ||
| requestParams[1] | ||
| ); | ||
| const signedTypedDataParams = JSON.parse(requestParams[1]); | ||
| response = await this.wallet.signTypedData({ | ||
@@ -960,2 +1060,24 @@ account: requestParams[0], | ||
| } | ||
| // EIP-5792 Wallet Call API | ||
| case "wallet_getCapabilities": { | ||
| const capabilityParams = Array.isArray(args.params) ? args.params : void 0; | ||
| response = this.getCapabilities(capabilityParams); | ||
| break; | ||
| } | ||
| case "wallet_sendCalls": { | ||
| requestParams = args.params; | ||
| response = await this.sendCalls(requestParams[0]); | ||
| break; | ||
| } | ||
| case "wallet_getCallsStatus": { | ||
| requestParams = args.params; | ||
| response = await this.getCallsStatus(requestParams[0]); | ||
| break; | ||
| } | ||
| case "wallet_showCallsStatus": { | ||
| requestParams = args.params; | ||
| await this.showCallsStatus(requestParams[0]); | ||
| response = null; | ||
| break; | ||
| } | ||
| // TODO: not yet implemented or unclear if we support | ||
@@ -968,6 +1090,2 @@ case "eth_ecRecover": | ||
| case "wallet_watchAsset": | ||
| case "wallet_sendCalls": | ||
| case "wallet_getCallsStatus": | ||
| case "wallet_getCapabilities": | ||
| case "wallet_showCallsStatus": | ||
| case "wallet_grantPermissions": | ||
@@ -983,5 +1101,3 @@ throw rpcErrors4.methodNotSupported("Not yet implemented."); | ||
| if (!this.wallet.chain.rpcUrl) | ||
| throw rpcErrors4.internal( | ||
| `RPC URL missing for current chain (${this.wallet.chain.id})` | ||
| ); | ||
| throw rpcErrors4.internal(`RPC URL missing for current chain (${this.wallet.chain.id})`); | ||
| return fetchRpcRequest(args, this.wallet.chain.rpcUrl); | ||
@@ -1000,2 +1116,40 @@ } | ||
| } | ||
| // EIP-5792 Implementation Methods - delegating to wallet | ||
| getCapabilities(params) { | ||
| if (!this.wallet) { | ||
| throw providerErrors2.unauthorized(); | ||
| } | ||
| const requestedChainIds = params?.[0]; | ||
| return this.wallet.getCapabilities(requestedChainIds); | ||
| } | ||
| async sendCalls(params) { | ||
| if (!this.wallet) { | ||
| throw providerErrors2.unauthorized(); | ||
| } | ||
| try { | ||
| return await this.wallet.sendCalls(params); | ||
| } catch (error) { | ||
| throw rpcErrors4.transactionRejected(error instanceof Error ? error.message : String(error)); | ||
| } | ||
| } | ||
| async getCallsStatus(batchId) { | ||
| if (!this.wallet) { | ||
| throw providerErrors2.unauthorized(); | ||
| } | ||
| try { | ||
| return await this.wallet.getCallsStatus(batchId); | ||
| } catch (error) { | ||
| throw rpcErrors4.invalidParams(error instanceof Error ? error.message : String(error)); | ||
| } | ||
| } | ||
| async showCallsStatus(batchId) { | ||
| if (!this.wallet) { | ||
| throw providerErrors2.unauthorized(); | ||
| } | ||
| try { | ||
| await this.wallet.showCallsStatus(batchId); | ||
| } catch (error) { | ||
| throw rpcErrors4.invalidParams(error instanceof Error ? error.message : String(error)); | ||
| } | ||
| } | ||
| async disconnect() { | ||
@@ -1026,2 +1180,3 @@ if (this.wallet) { | ||
| SDK_VERSION, | ||
| STORAGE_CALL_BATCHES_KEY, | ||
| STORAGE_ETH_ACCOUNTS_KEY, | ||
@@ -1028,0 +1183,0 @@ STORAGE_ETH_ACTIVE_CHAIN_KEY, |
+5
-9
| { | ||
| "name": "@gemini-wallet/core", | ||
| "version": "0.3.1", | ||
| "version": "0.3.2", | ||
| "description": "Core SDK for Gemini Wallet integration with popup communication", | ||
@@ -47,21 +47,17 @@ "main": "./dist/index.cjs", | ||
| "@eslint/eslintrc": "3.3.1", | ||
| "@eslint/js": "9.33.0", | ||
| "@next/eslint-plugin-next": "15.4.7", | ||
| "@eslint/js": "9.38.0", | ||
| "@types/node": "22.13.0", | ||
| "dotenv-cli": "10.0.0", | ||
| "esbuild-plugin-replace": "1.4.0", | ||
| "eslint": "9.33.0", | ||
| "eslint": "9.38.0", | ||
| "eslint-config-prettier": "10.1.8", | ||
| "eslint-config-turbo": "2.5.6", | ||
| "eslint-plugin-import": "2.32.0", | ||
| "eslint-plugin-jsx-a11y": "6.10.2", | ||
| "eslint-plugin-only-warn": "1.1.0", | ||
| "eslint-plugin-prettier": "5.5.4", | ||
| "eslint-plugin-react": "7.37.5", | ||
| "eslint-plugin-react-hooks": "5.2.0", | ||
| "eslint-plugin-simple-import-sort": "12.1.1", | ||
| "eslint-plugin-sort-keys-fix": "1.1.2", | ||
| "globals": "16.3.0", | ||
| "globals": "16.4.0", | ||
| "prettier": "3.6.2", | ||
| "tsup": "8.4.0", | ||
| "tsup": "8.5.0", | ||
| "typescript": "5.5.3", | ||
@@ -68,0 +64,0 @@ "typescript-eslint": "8.40.0", |
+4
-0
@@ -48,2 +48,3 @@ # @gemini-wallet/core | ||
| url: "https://mydapp.com", | ||
| icon: "https://mydapp.com/icon.png", | ||
| }, | ||
@@ -75,2 +76,3 @@ }), | ||
| url: "https://mydapp.com", | ||
| icon: "https://mydapp.com/icon.png", | ||
| }, | ||
@@ -114,2 +116,3 @@ chain: { id: 42161 }, // Arbitrum One | ||
| url: "https://mydapp.com", | ||
| icon: "https://mydapp.com/icon.png", | ||
| }, | ||
@@ -146,2 +149,3 @@ chain: { id: 42161 }, | ||
| url: "https://mydapp.com", | ||
| icon: "https://mydapp.com/icon.png", | ||
| }, | ||
@@ -148,0 +152,0 @@ }); |
+50
-82
| import { providerErrors } from "@metamask/rpc-errors"; | ||
| import { | ||
| afterEach, | ||
| beforeEach, | ||
| describe, | ||
| expect, | ||
| it, | ||
| mock, | ||
| spyOn, | ||
| } from "bun:test"; | ||
| import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test"; | ||
| import { Communicator } from "./communicator"; | ||
| import { DEFAULT_CHAIN_ID } from "./constants"; | ||
| import { | ||
| AppMetadata, | ||
| GeminiSdkEvent, | ||
| GeminiSdkMessage, | ||
| GeminiSdkMessageResponse, | ||
| } from "./types"; | ||
| import { AppMetadata, GeminiSdkEvent, GeminiSdkMessage, GeminiSdkMessageResponse } from "./types"; | ||
| import { SDK_BACKEND_URL, SDK_VERSION } from "./utils"; | ||
@@ -54,8 +41,5 @@ | ||
| // Helper to simulate message events | ||
| const simulateMessage = ( | ||
| data: any, | ||
| origin: string = new URL(SDK_BACKEND_URL).origin, | ||
| ) => { | ||
| const simulateMessage = (data: any, origin: string = new URL(SDK_BACKEND_URL).origin) => { | ||
| const event = new MessageEvent("message", { data, origin }); | ||
| messageListeners.forEach((listener) => listener(event)); | ||
| messageListeners.forEach(listener => listener(event)); | ||
| }; | ||
@@ -72,24 +56,20 @@ | ||
| // Mock window event listeners | ||
| spyOn(window, "addEventListener").mockImplementation( | ||
| (event: string, listener: any) => { | ||
| if (event === "message") { | ||
| messageListeners.push(listener); | ||
| } | ||
| }, | ||
| ); | ||
| spyOn(window, "addEventListener").mockImplementation((event: string, listener: any) => { | ||
| if (event === "message") { | ||
| messageListeners.push(listener); | ||
| } | ||
| }); | ||
| spyOn(window, "removeEventListener").mockImplementation( | ||
| (event: string, listener: any) => { | ||
| if (event === "message") { | ||
| const index = messageListeners.indexOf(listener); | ||
| if (index > -1) { | ||
| messageListeners.splice(index, 1); | ||
| } | ||
| spyOn(window, "removeEventListener").mockImplementation((event: string, listener: any) => { | ||
| if (event === "message") { | ||
| const index = messageListeners.indexOf(listener); | ||
| if (index > -1) { | ||
| messageListeners.splice(index, 1); | ||
| } | ||
| }, | ||
| ); | ||
| } | ||
| }); | ||
| appMetadata = { | ||
| appIcon: "https://test.com/icon.png", | ||
| appName: "Test App", | ||
| icon: "https://test.com/icon.png", | ||
| name: "Test App", | ||
| }; | ||
@@ -222,6 +202,3 @@ | ||
| await communicator.postMessage(message); | ||
| expect(mockPopup.postMessage).toHaveBeenCalledWith( | ||
| message, | ||
| new URL(SDK_BACKEND_URL).origin, | ||
| ); | ||
| expect(mockPopup.postMessage).toHaveBeenCalledWith(message, new URL(SDK_BACKEND_URL).origin); | ||
| }); | ||
@@ -248,6 +225,5 @@ }); | ||
| const responsePromise = communicator.postRequestAndWaitForResponse< | ||
| GeminiSdkMessage, | ||
| GeminiSdkMessageResponse | ||
| >(request); | ||
| const responsePromise = communicator.postRequestAndWaitForResponse<GeminiSdkMessage, GeminiSdkMessageResponse>( | ||
| request, | ||
| ); | ||
@@ -286,6 +262,5 @@ // Simulate response | ||
| const responsePromise = communicator.postRequestAndWaitForResponse< | ||
| GeminiSdkMessage, | ||
| GeminiSdkMessageResponse | ||
| >(request); | ||
| const responsePromise = communicator.postRequestAndWaitForResponse<GeminiSdkMessage, GeminiSdkMessageResponse>( | ||
| request, | ||
| ); | ||
@@ -313,6 +288,5 @@ // Send wrong response | ||
| it("should filter messages by predicate", async () => { | ||
| const messagePromise = communicator.onMessage< | ||
| GeminiSdkMessage, | ||
| GeminiSdkMessageResponse | ||
| >((message) => message.event === GeminiSdkEvent.SDK_CONNECT_RESPONSE); | ||
| const messagePromise = communicator.onMessage<GeminiSdkMessage, GeminiSdkMessageResponse>( | ||
| message => message.event === GeminiSdkEvent.SDK_CONNECT_RESPONSE, | ||
| ); | ||
@@ -338,6 +312,5 @@ // Send non-matching message | ||
| it("should ignore messages from wrong origin", async () => { | ||
| const messagePromise = communicator.onMessage< | ||
| GeminiSdkMessage, | ||
| GeminiSdkMessageResponse | ||
| >((message) => message.event === GeminiSdkEvent.SDK_CONNECT_RESPONSE); | ||
| const messagePromise = communicator.onMessage<GeminiSdkMessage, GeminiSdkMessageResponse>( | ||
| message => message.event === GeminiSdkEvent.SDK_CONNECT_RESPONSE, | ||
| ); | ||
@@ -366,6 +339,5 @@ // Send from wrong origin | ||
| const messagePromise = communicator.onMessage< | ||
| GeminiSdkMessage, | ||
| GeminiSdkMessageResponse | ||
| >((message) => message.event === GeminiSdkEvent.SDK_CONNECT_RESPONSE); | ||
| const messagePromise = communicator.onMessage<GeminiSdkMessage, GeminiSdkMessageResponse>( | ||
| message => message.event === GeminiSdkEvent.SDK_CONNECT_RESPONSE, | ||
| ); | ||
@@ -404,3 +376,3 @@ // Should have added a listener | ||
| // Wait for event processing | ||
| await new Promise((resolve) => setTimeout(resolve, 10)); | ||
| await new Promise(resolve => setTimeout(resolve, 10)); | ||
@@ -420,6 +392,5 @@ expect(onDisconnectCallback).toHaveBeenCalled(); | ||
| // Start a pending request | ||
| const pendingPromise = communicator.onMessage< | ||
| GeminiSdkMessage, | ||
| GeminiSdkMessageResponse | ||
| >((message) => message.event === ("WILL_NEVER_ARRIVE" as any)); | ||
| const pendingPromise = communicator.onMessage<GeminiSdkMessage, GeminiSdkMessageResponse>( | ||
| message => message.event === ("WILL_NEVER_ARRIVE" as any), | ||
| ); | ||
@@ -450,15 +421,13 @@ // Simulate popup unloaded | ||
| // Add multiple listeners - create promises but don't await yet | ||
| const promise1 = communicator.onMessage< | ||
| GeminiSdkMessage, | ||
| GeminiSdkMessageResponse | ||
| >((message) => message.event === ("EVENT1" as any)); | ||
| const promise2 = communicator.onMessage< | ||
| GeminiSdkMessage, | ||
| GeminiSdkMessageResponse | ||
| >((message) => message.event === ("EVENT2" as any)); | ||
| const promise1 = communicator.onMessage<GeminiSdkMessage, GeminiSdkMessageResponse>( | ||
| message => message.event === ("EVENT1" as any), | ||
| ); | ||
| const promise2 = communicator.onMessage<GeminiSdkMessage, GeminiSdkMessageResponse>( | ||
| message => message.event === ("EVENT2" as any), | ||
| ); | ||
| // Collect rejection errors | ||
| const errors: any[] = []; | ||
| promise1.catch((err) => errors.push(err)); | ||
| promise2.catch((err) => errors.push(err)); | ||
| promise1.catch(err => errors.push(err)); | ||
| promise2.catch(err => errors.push(err)); | ||
@@ -475,3 +444,3 @@ const initialListenerCount = messageListeners.length; | ||
| // Wait for cleanup and error propagation | ||
| await new Promise((resolve) => setTimeout(resolve, 50)); | ||
| await new Promise(resolve => setTimeout(resolve, 50)); | ||
@@ -496,6 +465,5 @@ // Both promises should have rejected with user rejection error | ||
| // Start a request that will be rejected | ||
| const pendingPromise = communicator.onMessage< | ||
| GeminiSdkMessage, | ||
| GeminiSdkMessageResponse | ||
| >((message) => message.event === ("WILL_NEVER_ARRIVE" as any)); | ||
| const pendingPromise = communicator.onMessage<GeminiSdkMessage, GeminiSdkMessageResponse>( | ||
| message => message.event === ("WILL_NEVER_ARRIVE" as any), | ||
| ); | ||
@@ -502,0 +470,0 @@ // Force rejection by simulating unload |
+6
-18
@@ -23,6 +23,3 @@ import { providerErrors, rpcErrors } from "@metamask/rpc-errors"; | ||
| private popup: Window | null = null; | ||
| private listeners = new Map< | ||
| (_: MessageEvent) => void, | ||
| { reject: (_: Error) => void } | ||
| >(); | ||
| private listeners = new Map<(_: MessageEvent) => void, { reject: (_: Error) => void }>(); | ||
| private onDisconnectCallback?: () => void; | ||
@@ -43,11 +40,6 @@ | ||
| // posts a request to the popup window and waits for a response | ||
| postRequestAndWaitForResponse = async < | ||
| M extends GeminiSdkMessage, | ||
| R extends GeminiSdkMessageResponse, | ||
| >( | ||
| postRequestAndWaitForResponse = async <M extends GeminiSdkMessage, R extends GeminiSdkMessageResponse>( | ||
| request: GeminiSdkMessage, | ||
| ): Promise<R> => { | ||
| const responsePromise = this.onMessage<M, R>( | ||
| ({ requestId }) => requestId === request.requestId, | ||
| ); | ||
| const responsePromise = this.onMessage<M, R>(({ requestId }) => requestId === request.requestId); | ||
| this.postMessage(request); | ||
@@ -102,5 +94,3 @@ return await responsePromise; | ||
| // setup popup closed listener in case user closes window without explicit response | ||
| this.onMessage<GeminiSdkMessage, GeminiSdkMessageResponse>( | ||
| ({ event }) => event === GeminiSdkEvent.POPUP_UNLOADED, | ||
| ) | ||
| this.onMessage<GeminiSdkMessage, GeminiSdkMessageResponse>(({ event }) => event === GeminiSdkEvent.POPUP_UNLOADED) | ||
| .then(this.onRequestCancelled) | ||
@@ -110,5 +100,3 @@ .catch(() => {}); | ||
| // setup account disconnect listener in case user requests disconnect from within popup | ||
| this.onMessage<GeminiSdkMessage, GeminiSdkMessageResponse>( | ||
| ({ event }) => event === GeminiSdkEvent.SDK_DISCONNECT, | ||
| ) | ||
| this.onMessage<GeminiSdkMessage, GeminiSdkMessageResponse>(({ event }) => event === GeminiSdkEvent.SDK_DISCONNECT) | ||
| .then(() => { | ||
@@ -125,3 +113,3 @@ // invoke disconnect callback passed in from wallet | ||
| ) | ||
| .then((message) => { | ||
| .then(message => { | ||
| // report app metadata to backend upon load complete | ||
@@ -128,0 +116,0 @@ this.postMessage({ |
+2
-7
@@ -18,5 +18,3 @@ import { | ||
| export const SDK_BACKEND_URL = | ||
| (typeof process !== "undefined" && process.env?.SDK_BACKEND_URL) || | ||
| DEFAULT_BACKEND_URL; | ||
| export const SDK_BACKEND_URL = process.env?.SDK_BACKEND_URL || DEFAULT_BACKEND_URL; | ||
| export const ENS_API_URL = "https://horizon-api.gemini.com/api/ens"; | ||
@@ -45,6 +43,3 @@ export const SDK_VERSION = packageJson.version; | ||
| // All supported chain IDs | ||
| export const SUPPORTED_CHAIN_IDS = [ | ||
| ...Object.values(MAINNET_CHAIN_IDS), | ||
| ...Object.values(TESTNET_CHAIN_IDS), | ||
| ]; | ||
| export const SUPPORTED_CHAIN_IDS = [...Object.values(MAINNET_CHAIN_IDS), ...Object.values(TESTNET_CHAIN_IDS)]; | ||
@@ -51,0 +46,0 @@ // Helper function to get default RPC URL for a chain using viem chains |
+10
-11
@@ -15,2 +15,3 @@ // Main exports | ||
| GeminiStorage, | ||
| STORAGE_CALL_BATCHES_KEY, | ||
| STORAGE_ETH_ACCOUNTS_KEY, | ||
@@ -29,2 +30,4 @@ STORAGE_ETH_ACTIVE_CHAIN_KEY, | ||
| AppMetadata, | ||
| Call, | ||
| CallBatchMetadata, | ||
| Chain, | ||
@@ -36,2 +39,3 @@ ConnectResponse, | ||
| GeminiSdkMessageResponse, | ||
| GeminiSdkSendBatchCalls, | ||
| GeminiSdkSendTransaction, | ||
@@ -41,2 +45,3 @@ GeminiSdkSignMessage, | ||
| GeminiSdkSwitchChain, | ||
| GetCallsStatusResponse, | ||
| ProviderEventCallback, | ||
@@ -48,2 +53,4 @@ ProviderEventMap, | ||
| RpcRequestArgs, | ||
| SendCallsParams, | ||
| SendCallsResponse, | ||
| SendTransactionResponse, | ||
@@ -53,2 +60,3 @@ SignMessageResponse, | ||
| SwitchChainResponse, | ||
| WalletCapabilities, | ||
| } from "./types"; | ||
@@ -58,6 +66,3 @@ export { GeminiSdkEvent, PlatformType, ProviderEventEmitter } from "./types"; | ||
| // Utility exports | ||
| export type { | ||
| CalculateWalletAddressParams, | ||
| WebAuthnValidatorData, | ||
| } from "./utils"; | ||
| export type { CalculateWalletAddressParams, WebAuthnValidatorData } from "./utils"; | ||
| export { | ||
@@ -81,8 +86,2 @@ base64ToHex, | ||
| // Constants | ||
| export { | ||
| DEFAULT_CHAIN_ID, | ||
| POPUP_HEIGHT, | ||
| POPUP_WIDTH, | ||
| SDK_BACKEND_URL, | ||
| SDK_VERSION, | ||
| } from "./constants"; | ||
| export { DEFAULT_CHAIN_ID, POPUP_HEIGHT, POPUP_WIDTH, SDK_BACKEND_URL, SDK_VERSION } from "./constants"; |
@@ -8,8 +8,6 @@ import { describe, expect, mock, test } from "bun:test"; | ||
| describe("GeminiProviderConfig", () => { | ||
| const createMockConfig = ( | ||
| overrides: Partial<GeminiProviderConfig> = {}, | ||
| ): GeminiProviderConfig => ({ | ||
| const createMockConfig = (overrides: Partial<GeminiProviderConfig> = {}): GeminiProviderConfig => ({ | ||
| appMetadata: { | ||
| description: "Test Description", | ||
| icons: [], | ||
| icon: "https://test.com/icon.png", | ||
| name: "Test App", | ||
@@ -53,3 +51,3 @@ url: "https://test.com", | ||
| description: "Custom Description", | ||
| icons: ["icon1.png", "icon2.png"], | ||
| icon: "https://custom.com/icon.png", | ||
| name: "Custom App", | ||
@@ -56,0 +54,0 @@ url: "https://custom.com", |
@@ -13,6 +13,4 @@ import { errorCodes } from "@metamask/rpc-errors"; | ||
| const mockAddress = "0xAfEDA61dB9e162293b2eF2C2bC5A800b37Bb5E4a" as Address; | ||
| const mockTxHash = | ||
| "0x5de3752c591ecc35d1046f3aca2eba1ba5bdcfb786639a8661e9ecb823675743" as Hex; | ||
| const mockSigHash = | ||
| "0x020d671b80fbd20466d8cb65cef79a24e3bca3fdf82e9dd89d78e7a4c4c045b" as Hex; | ||
| const mockTxHash = "0x5de3752c591ecc35d1046f3aca2eba1ba5bdcfb786639a8661e9ecb823675743" as Hex; | ||
| const mockSigHash = "0x020d671b80fbd20466d8cb65cef79a24e3bca3fdf82e9dd89d78e7a4c4c045b" as Hex; | ||
@@ -80,3 +78,3 @@ // Mock dependencies - Create a more comprehensive mock of GeminiWallet | ||
| providerConfig = { | ||
| appMetadata: { appName: "Test App" }, | ||
| appMetadata: { name: "Test App" }, | ||
| chain: { id: DEFAULT_CHAIN_ID }, | ||
@@ -83,0 +81,0 @@ onDisconnectCallback: mock(), |
+77
-33
@@ -0,8 +1,3 @@ | ||
| import { errorCodes, providerErrors, rpcErrors, serializeError } from "@metamask/rpc-errors"; | ||
| import { | ||
| errorCodes, | ||
| providerErrors, | ||
| rpcErrors, | ||
| serializeError, | ||
| } from "@metamask/rpc-errors"; | ||
| import { | ||
| type Address, | ||
@@ -16,25 +11,18 @@ type Hex, | ||
| import { DEFAULT_CHAIN_ID } from "../constants"; | ||
| import { GeminiStorage, STORAGE_ETH_ACCOUNTS_KEY, STORAGE_ETH_ACTIVE_CHAIN_KEY } from "../storage"; | ||
| import { | ||
| GeminiStorage, | ||
| STORAGE_ETH_ACCOUNTS_KEY, | ||
| STORAGE_ETH_ACTIVE_CHAIN_KEY, | ||
| } from "../storage"; | ||
| import { | ||
| type GeminiProviderConfig, | ||
| type GetCallsStatusResponse, | ||
| ProviderEventEmitter, | ||
| type ProviderInterface, | ||
| type RpcRequestArgs, | ||
| type SendCallsParams, | ||
| type SendCallsResponse, | ||
| type WalletCapabilities, | ||
| } from "../types"; | ||
| import { hexStringFromNumber } from "../utils"; | ||
| import { GeminiWallet } from "../wallets"; | ||
| import { | ||
| convertSendValuesToBigInt, | ||
| fetchRpcRequest, | ||
| validateRpcRequestArgs, | ||
| } from "./provider.utils"; | ||
| import { convertSendValuesToBigInt, fetchRpcRequest, validateRpcRequestArgs } from "./provider.utils"; | ||
| export class GeminiWalletProvider | ||
| extends ProviderEventEmitter | ||
| implements ProviderInterface | ||
| { | ||
| export class GeminiWalletProvider extends ProviderEventEmitter implements ProviderInterface { | ||
| private readonly config: GeminiProviderConfig; | ||
@@ -137,5 +125,3 @@ private wallet: GeminiWallet | null = null; | ||
| // Handle both standard EIP-3326 format [{ chainId: hex }] and legacy format { id: number } | ||
| const rawParams = args.params as | ||
| | [{ chainId: string }] | ||
| | { id: number }; | ||
| const rawParams = args.params as [{ chainId: string }] | { id: number }; | ||
| let chainId: number; | ||
@@ -176,5 +162,3 @@ | ||
| requestParams = args.params as Array<Hex | Address>; | ||
| const signedTypedDataParams = JSON.parse( | ||
| requestParams[1] as string, | ||
| ) as SignTypedDataParameters; | ||
| const signedTypedDataParams = JSON.parse(requestParams[1] as string) as SignTypedDataParameters; | ||
| response = await this.wallet.signTypedData({ | ||
@@ -194,2 +178,25 @@ account: requestParams[0] as Address, | ||
| } | ||
| // EIP-5792 Wallet Call API | ||
| case "wallet_getCapabilities": { | ||
| const capabilityParams = Array.isArray(args.params) ? args.params : undefined; | ||
| response = this.getCapabilities(capabilityParams); | ||
| break; | ||
| } | ||
| case "wallet_sendCalls": { | ||
| requestParams = args.params as [SendCallsParams]; | ||
| response = await this.sendCalls(requestParams[0]); | ||
| break; | ||
| } | ||
| case "wallet_getCallsStatus": { | ||
| requestParams = args.params as [string]; | ||
| response = await this.getCallsStatus(requestParams[0]); | ||
| break; | ||
| } | ||
| case "wallet_showCallsStatus": { | ||
| requestParams = args.params as [string]; | ||
| await this.showCallsStatus(requestParams[0]); | ||
| response = null; | ||
| break; | ||
| } | ||
| // TODO: not yet implemented or unclear if we support | ||
@@ -202,6 +209,2 @@ case "eth_ecRecover": | ||
| case "wallet_watchAsset": | ||
| case "wallet_sendCalls": | ||
| case "wallet_getCallsStatus": | ||
| case "wallet_getCapabilities": | ||
| case "wallet_showCallsStatus": | ||
| case "wallet_grantPermissions": | ||
@@ -219,5 +222,3 @@ throw rpcErrors.methodNotSupported("Not yet implemented."); | ||
| if (!this.wallet.chain.rpcUrl) | ||
| throw rpcErrors.internal( | ||
| `RPC URL missing for current chain (${this.wallet.chain.id})`, | ||
| ); | ||
| throw rpcErrors.internal(`RPC URL missing for current chain (${this.wallet.chain.id})`); | ||
| return fetchRpcRequest(args, this.wallet.chain.rpcUrl); | ||
@@ -239,2 +240,45 @@ } | ||
| // EIP-5792 Implementation Methods - delegating to wallet | ||
| private getCapabilities(params?: readonly unknown[]): WalletCapabilities { | ||
| if (!this.wallet) { | ||
| throw providerErrors.unauthorized(); | ||
| } | ||
| const requestedChainIds = params?.[0] as string[] | undefined; | ||
| return this.wallet.getCapabilities(requestedChainIds); | ||
| } | ||
| private async sendCalls(params: SendCallsParams): Promise<SendCallsResponse> { | ||
| if (!this.wallet) { | ||
| throw providerErrors.unauthorized(); | ||
| } | ||
| try { | ||
| return await this.wallet.sendCalls(params); | ||
| } catch (error) { | ||
| throw rpcErrors.transactionRejected(error instanceof Error ? error.message : String(error)); | ||
| } | ||
| } | ||
| private async getCallsStatus(batchId: string): Promise<GetCallsStatusResponse> { | ||
| if (!this.wallet) { | ||
| throw providerErrors.unauthorized(); | ||
| } | ||
| try { | ||
| return await this.wallet.getCallsStatus(batchId); | ||
| } catch (error) { | ||
| throw rpcErrors.invalidParams(error instanceof Error ? error.message : String(error)); | ||
| } | ||
| } | ||
| private async showCallsStatus(batchId: string): Promise<void> { | ||
| if (!this.wallet) { | ||
| throw providerErrors.unauthorized(); | ||
| } | ||
| try { | ||
| await this.wallet.showCallsStatus(batchId); | ||
| } catch (error) { | ||
| throw rpcErrors.invalidParams(error instanceof Error ? error.message : String(error)); | ||
| } | ||
| } | ||
| async disconnect() { | ||
@@ -241,0 +285,0 @@ // If wallet exists, let it handle its own storage cleanup |
| import { beforeEach, describe, expect, it, mock } from "bun:test"; | ||
| import { | ||
| convertSendValuesToBigInt, | ||
| fetchRpcRequest, | ||
| validateRpcRequestArgs, | ||
| } from "./provider.utils"; | ||
| import { convertSendValuesToBigInt, fetchRpcRequest, validateRpcRequestArgs } from "./provider.utils"; | ||
| // Set up global window mock before tests | ||
| (global as any).window = { | ||
| fetch: () => | ||
| Promise.resolve({ json: () => Promise.resolve({ result: "success" }) }), | ||
| fetch: () => Promise.resolve({ json: () => Promise.resolve({ result: "success" }) }), | ||
| }; | ||
@@ -18,47 +13,33 @@ | ||
| it("should throw an error if args is null", () => { | ||
| expect(() => validateRpcRequestArgs(null)).toThrow( | ||
| "Expected a single, non-array, object argument.", | ||
| ); | ||
| expect(() => validateRpcRequestArgs(null)).toThrow("Expected a single, non-array, object argument."); | ||
| }); | ||
| it("should throw an error if args is not an object", () => { | ||
| expect(() => validateRpcRequestArgs(42)).toThrow( | ||
| "Expected a single, non-array, object argument.", | ||
| ); | ||
| expect(() => validateRpcRequestArgs(42)).toThrow("Expected a single, non-array, object argument."); | ||
| }); | ||
| it("should throw an error if args is an array", () => { | ||
| expect(() => validateRpcRequestArgs([])).toThrow( | ||
| "Expected a single, non-array, object argument.", | ||
| ); | ||
| expect(() => validateRpcRequestArgs([])).toThrow("Expected a single, non-array, object argument."); | ||
| }); | ||
| it("should throw an error if method is missing", () => { | ||
| expect(() => validateRpcRequestArgs({})).toThrow( | ||
| "'args.method' must be a non-empty string.", | ||
| ); | ||
| expect(() => validateRpcRequestArgs({})).toThrow("'args.method' must be a non-empty string."); | ||
| }); | ||
| it("should throw an error if method is not a string", () => { | ||
| expect(() => validateRpcRequestArgs({ method: 123 })).toThrow( | ||
| "'args.method' must be a non-empty string.", | ||
| ); | ||
| expect(() => validateRpcRequestArgs({ method: 123 })).toThrow("'args.method' must be a non-empty string."); | ||
| }); | ||
| it("should throw an error if method is an empty string", () => { | ||
| expect(() => validateRpcRequestArgs({ method: "" })).toThrow( | ||
| "'args.method' must be a non-empty string.", | ||
| ); | ||
| expect(() => validateRpcRequestArgs({ method: "" })).toThrow("'args.method' must be a non-empty string."); | ||
| }); | ||
| it("should throw an error if params is not an object or array", () => { | ||
| expect(() => | ||
| validateRpcRequestArgs({ method: "testMethod", params: 123 }), | ||
| ).toThrow("'args.params' must be an object or array if provided."); | ||
| expect(() => validateRpcRequestArgs({ method: "testMethod", params: 123 })).toThrow( | ||
| "'args.params' must be an object or array if provided.", | ||
| ); | ||
| }); | ||
| it("should not throw an error for a valid request with an array params", () => { | ||
| expect(() => | ||
| validateRpcRequestArgs({ method: "testMethod", params: [] }), | ||
| ).not.toThrow(); | ||
| expect(() => validateRpcRequestArgs({ method: "testMethod", params: [] })).not.toThrow(); | ||
| }); | ||
@@ -76,5 +57,3 @@ | ||
| it("should not throw an error for a valid request without params", () => { | ||
| expect(() => | ||
| validateRpcRequestArgs({ method: "testMethod" }), | ||
| ).not.toThrow(); | ||
| expect(() => validateRpcRequestArgs({ method: "testMethod" })).not.toThrow(); | ||
| }); | ||
@@ -81,0 +60,0 @@ }); |
@@ -12,6 +12,3 @@ import { rpcErrors } from "@metamask/rpc-errors"; | ||
| */ | ||
| export const fetchRpcRequest = async ( | ||
| request: RpcRequestArgs, | ||
| rpcUrl: string, | ||
| ) => { | ||
| export const fetchRpcRequest = async (request: RpcRequestArgs, rpcUrl: string) => { | ||
| const requestBody = { | ||
@@ -41,5 +38,3 @@ ...request, | ||
| */ | ||
| export function validateRpcRequestArgs( | ||
| args: unknown, | ||
| ): asserts args is RpcRequestArgs { | ||
| export function validateRpcRequestArgs(args: unknown): asserts args is RpcRequestArgs { | ||
| if (!args || typeof args !== "object" || Array.isArray(args)) { | ||
@@ -59,7 +54,3 @@ throw rpcErrors.invalidParams({ | ||
| if ( | ||
| params !== undefined && | ||
| !Array.isArray(params) && | ||
| (typeof params !== "object" || params === null) | ||
| ) { | ||
| if (params !== undefined && !Array.isArray(params) && (typeof params !== "object" || params === null)) { | ||
| throw rpcErrors.invalidParams({ | ||
@@ -76,5 +67,3 @@ message: "'args.params' must be an object or array if provided.", | ||
| */ | ||
| export function convertSendValuesToBigInt( | ||
| tx: TransactionRequest, | ||
| ): TransactionRequest { | ||
| export function convertSendValuesToBigInt(tx: TransactionRequest): TransactionRequest { | ||
| const FIELDS_TO_NORMALIZE: (keyof Pick< | ||
@@ -81,0 +70,0 @@ TransactionRequest, |
| export { GeminiStorage, type GeminiStorageConfig } from "./storage"; | ||
| export { | ||
| type IStorage, | ||
| STORAGE_CALL_BATCHES_KEY, | ||
| STORAGE_ETH_ACCOUNTS_KEY, | ||
@@ -5,0 +6,0 @@ STORAGE_ETH_ACTIVE_CHAIN_KEY, |
@@ -1,14 +0,6 @@ | ||
| import { | ||
| afterEach, | ||
| beforeEach, | ||
| describe, | ||
| expect, | ||
| it, | ||
| mock, | ||
| spyOn, | ||
| } from "bun:test"; | ||
| import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test"; | ||
| // Mock dependencies | ||
| mock.module("../utils", () => ({ | ||
| safeJsonStringify: mock((obj) => JSON.stringify(obj)), | ||
| safeJsonStringify: mock(obj => JSON.stringify(obj)), | ||
| })); | ||
@@ -70,6 +62,3 @@ | ||
| await storage.setItem("test", "value"); | ||
| expect(mockLocalStorage.setItem).toHaveBeenCalledWith( | ||
| "@gemini.wallet.test", | ||
| "value", | ||
| ); | ||
| expect(mockLocalStorage.setItem).toHaveBeenCalledWith("@gemini.wallet.test", "value"); | ||
| }); | ||
@@ -86,6 +75,3 @@ }); | ||
| expect(safeJsonStringify).toHaveBeenCalledWith(testObject); | ||
| expect(mockLocalStorage.setItem).toHaveBeenCalledWith( | ||
| "@gemini.wallet.testKey", | ||
| JSON.stringify(testObject), | ||
| ); | ||
| expect(mockLocalStorage.setItem).toHaveBeenCalledWith("@gemini.wallet.testKey", JSON.stringify(testObject)); | ||
| }); | ||
@@ -98,5 +84,3 @@ | ||
| // Mock safeJsonStringify for this specific test | ||
| (safeJsonStringify as any).mockReturnValueOnce( | ||
| '{"id":"test","value":"123n"}', | ||
| ); | ||
| (safeJsonStringify as any).mockReturnValueOnce('{"id":"test","value":"123n"}'); | ||
@@ -106,6 +90,3 @@ await storage.storeObject("testKey", testObject); | ||
| expect(safeJsonStringify).toHaveBeenCalledWith(testObject); | ||
| expect(mockLocalStorage.setItem).toHaveBeenCalledWith( | ||
| "@gemini.wallet.testKey", | ||
| '{"id":"test","value":"123n"}', | ||
| ); | ||
| expect(mockLocalStorage.setItem).toHaveBeenCalledWith("@gemini.wallet.testKey", '{"id":"test","value":"123n"}'); | ||
| }); | ||
@@ -123,5 +104,3 @@ }); | ||
| expect(mockLocalStorage.getItem).toHaveBeenCalledWith( | ||
| "@gemini.wallet.testKey", | ||
| ); | ||
| expect(mockLocalStorage.getItem).toHaveBeenCalledWith("@gemini.wallet.testKey"); | ||
| expect(result).toEqual({ foo: "bar", num: 123 }); | ||
@@ -138,5 +117,3 @@ }); | ||
| expect(mockLocalStorage.getItem).toHaveBeenCalledWith( | ||
| "@gemini.wallet.nonExistentKey", | ||
| ); | ||
| expect(mockLocalStorage.getItem).toHaveBeenCalledWith("@gemini.wallet.nonExistentKey"); | ||
| expect(result).toEqual(fallback); | ||
@@ -153,11 +130,7 @@ }); | ||
| // Mock console.error to avoid test output pollution | ||
| const consoleErrorSpy = spyOn(console, "error").mockImplementation( | ||
| () => {}, | ||
| ); | ||
| const consoleErrorSpy = spyOn(console, "error").mockImplementation(() => {}); | ||
| const result = await storage.loadObject("invalidJsonKey", fallback); | ||
| expect(mockLocalStorage.getItem).toHaveBeenCalledWith( | ||
| "@gemini.wallet.invalidJsonKey", | ||
| ); | ||
| expect(mockLocalStorage.getItem).toHaveBeenCalledWith("@gemini.wallet.invalidJsonKey"); | ||
| expect(result).toEqual(fallback); | ||
@@ -178,6 +151,3 @@ | ||
| expect(mockLocalStorage.setItem).toHaveBeenCalledWith( | ||
| "@gemini.wallet.testKey", | ||
| "testValue", | ||
| ); | ||
| expect(mockLocalStorage.setItem).toHaveBeenCalledWith("@gemini.wallet.testKey", "testValue"); | ||
| }); | ||
@@ -190,6 +160,3 @@ | ||
| expect(mockLocalStorage.setItem).toHaveBeenCalledWith( | ||
| "@gemini.wallet.emptyKey", | ||
| "", | ||
| ); | ||
| expect(mockLocalStorage.setItem).toHaveBeenCalledWith("@gemini.wallet.emptyKey", ""); | ||
| }); | ||
@@ -206,5 +173,3 @@ }); | ||
| expect(mockLocalStorage.getItem).toHaveBeenCalledWith( | ||
| "@gemini.wallet.testKey", | ||
| ); | ||
| expect(mockLocalStorage.getItem).toHaveBeenCalledWith("@gemini.wallet.testKey"); | ||
| expect(result).toBe("testValue"); | ||
@@ -220,5 +185,3 @@ }); | ||
| expect(mockLocalStorage.getItem).toHaveBeenCalledWith( | ||
| "@gemini.wallet.nonExistentKey", | ||
| ); | ||
| expect(mockLocalStorage.getItem).toHaveBeenCalledWith("@gemini.wallet.nonExistentKey"); | ||
| expect(result).toBeNull(); | ||
@@ -234,5 +197,3 @@ }); | ||
| expect(mockLocalStorage.removeItem).toHaveBeenCalledWith( | ||
| "@gemini.wallet.testKey", | ||
| ); | ||
| expect(mockLocalStorage.removeItem).toHaveBeenCalledWith("@gemini.wallet.testKey"); | ||
| }); | ||
@@ -249,11 +210,5 @@ }); | ||
| expect(mockLocalStorage.removeItem).toHaveBeenCalledTimes(3); | ||
| expect(mockLocalStorage.removeItem).toHaveBeenCalledWith( | ||
| "@gemini.wallet.key1", | ||
| ); | ||
| expect(mockLocalStorage.removeItem).toHaveBeenCalledWith( | ||
| "@gemini.wallet.key2", | ||
| ); | ||
| expect(mockLocalStorage.removeItem).toHaveBeenCalledWith( | ||
| "@gemini.wallet.key3", | ||
| ); | ||
| expect(mockLocalStorage.removeItem).toHaveBeenCalledWith("@gemini.wallet.key1"); | ||
| expect(mockLocalStorage.removeItem).toHaveBeenCalledWith("@gemini.wallet.key2"); | ||
| expect(mockLocalStorage.removeItem).toHaveBeenCalledWith("@gemini.wallet.key3"); | ||
| }); | ||
@@ -275,5 +230,3 @@ | ||
| expect(mockLocalStorage.removeItem).toHaveBeenCalledTimes(1); | ||
| expect(mockLocalStorage.removeItem).toHaveBeenCalledWith( | ||
| "@gemini.wallet.singleKey", | ||
| ); | ||
| expect(mockLocalStorage.removeItem).toHaveBeenCalledWith("@gemini.wallet.singleKey"); | ||
| }); | ||
@@ -298,16 +251,9 @@ }); | ||
| await storage.setItem(STORAGE_ETH_ACCOUNTS_KEY, "accounts-data"); | ||
| expect(mockLocalStorage.setItem).toHaveBeenCalledWith( | ||
| "@gemini.wallet.eth-accounts", | ||
| "accounts-data", | ||
| ); | ||
| expect(mockLocalStorage.setItem).toHaveBeenCalledWith("@gemini.wallet.eth-accounts", "accounts-data"); | ||
| await storage.getItem(STORAGE_PASSKEY_CREDENTIAL_KEY); | ||
| expect(mockLocalStorage.getItem).toHaveBeenCalledWith( | ||
| "@gemini.wallet.passkey-credential", | ||
| ); | ||
| expect(mockLocalStorage.getItem).toHaveBeenCalledWith("@gemini.wallet.passkey-credential"); | ||
| await storage.removeItem(STORAGE_SETTINGS_KEY); | ||
| expect(mockLocalStorage.removeItem).toHaveBeenCalledWith( | ||
| "@gemini.wallet.settings", | ||
| ); | ||
| expect(mockLocalStorage.removeItem).toHaveBeenCalledWith("@gemini.wallet.settings"); | ||
| }); | ||
@@ -314,0 +260,0 @@ }); |
@@ -20,6 +20,3 @@ import { safeJsonStringify } from "../utils"; | ||
| constructor({ | ||
| scope = "@gemini", | ||
| module = "wallet", | ||
| }: GeminiStorageConfig = {}) { | ||
| constructor({ scope = "@gemini", module = "wallet" }: GeminiStorageConfig = {}) { | ||
| this.scope = scope; | ||
@@ -93,4 +90,4 @@ this.module = module; | ||
| public async removeItems(keys: string[]): Promise<void> { | ||
| await Promise.all(keys.map((key) => this.removeItem(key))); | ||
| await Promise.all(keys.map(key => this.removeItem(key))); | ||
| } | ||
| } |
@@ -51,6 +51,6 @@ /** | ||
| export const STORAGE_PASSKEY_CREDENTIAL_KEY = "passkey-credential"; | ||
| export const STORAGE_PRESERVED_PASSKEY_CREDENTIALS_KEY = | ||
| "preserved-passkey-credentials"; | ||
| export const STORAGE_PRESERVED_PASSKEY_CREDENTIALS_KEY = "preserved-passkey-credentials"; | ||
| export const STORAGE_SMART_ACCOUNT_KEY = "smart-account"; | ||
| export const STORAGE_SETTINGS_KEY = "settings"; | ||
| export const STORAGE_WC_REQUESTS_KEY = "wc-requests"; | ||
| export const STORAGE_CALL_BATCHES_KEY = "call-batches"; |
+110
-35
| import { EventEmitter } from "eventemitter3"; | ||
| import type { | ||
| Address, | ||
| Hex, | ||
| SignMessageParameters, | ||
| SignTypedDataParameters, | ||
| TransactionRequest, | ||
| } from "viem"; | ||
| import type { Address, Hex, SignMessageParameters, SignTypedDataParameters, TransactionRequest } from "viem"; | ||
@@ -27,11 +21,35 @@ import { type IStorage } from "./storage/storageInterface"; | ||
| SDK_CURRENT_ACCOUNT = "SDK_CURRENT_ACCOUNT", | ||
| // EIP-5792 events | ||
| SDK_SEND_BATCH_CALLS = "SDK_SEND_BATCH_CALLS", | ||
| SDK_GET_CAPABILITIES = "SDK_GET_CAPABILITIES", | ||
| SDK_GET_CALLS_STATUS = "SDK_GET_CALLS_STATUS", | ||
| SDK_SHOW_CALLS_STATUS = "SDK_SHOW_CALLS_STATUS", | ||
| } | ||
| export interface AppMetadata { | ||
| appName?: string; | ||
| appLogoUrl?: string; | ||
| /** | ||
| * The name of your application | ||
| */ | ||
| name?: string; | ||
| /** | ||
| * The description of your application (optional) | ||
| */ | ||
| description?: string; | ||
| /** | ||
| * URL of your application | ||
| */ | ||
| url?: string; | ||
| icons?: string[]; | ||
| /** | ||
| * URL to your application's icon or logo | ||
| */ | ||
| icon?: string; | ||
| /** | ||
| * @deprecated Use `name` instead | ||
| */ | ||
| appName?: string; | ||
| /** | ||
| * @deprecated Use `icon` instead | ||
| */ | ||
| appLogoUrl?: string; | ||
| } | ||
@@ -90,16 +108,8 @@ | ||
| export class ProviderEventEmitter extends EventEmitter< | ||
| keyof ProviderEventMap | ||
| > {} | ||
| export class ProviderEventEmitter extends EventEmitter<keyof ProviderEventMap> {} | ||
| export interface ProviderInterface extends ProviderEventEmitter { | ||
| disconnect(): Promise<void>; | ||
| emit<K extends keyof ProviderEventMap>( | ||
| event: K, | ||
| ...args: [ProviderEventMap[K]] | ||
| ): boolean; | ||
| on<K extends keyof ProviderEventMap>( | ||
| event: K, | ||
| listener: (_: ProviderEventMap[K]) => void, | ||
| ): this; | ||
| emit<K extends keyof ProviderEventMap>(event: K, ...args: [ProviderEventMap[K]]): boolean; | ||
| on<K extends keyof ProviderEventMap>(event: K, listener: (_: ProviderEventMap[K]) => void): this; | ||
| request(args: RpcRequestArgs): Promise<any>; | ||
@@ -123,29 +133,23 @@ } | ||
| export interface ConnectResponse | ||
| extends Omit<GeminiSdkMessageResponse, "data"> { | ||
| export interface ConnectResponse extends Omit<GeminiSdkMessageResponse, "data"> { | ||
| data: { address: Address }; | ||
| } | ||
| export interface SendTransactionResponse | ||
| extends Omit<GeminiSdkMessageResponse, "data"> { | ||
| export interface SendTransactionResponse extends Omit<GeminiSdkMessageResponse, "data"> { | ||
| data: { hash?: Hex; error?: string }; | ||
| } | ||
| export interface SignMessageResponse | ||
| extends Omit<GeminiSdkMessageResponse, "data"> { | ||
| export interface SignMessageResponse extends Omit<GeminiSdkMessageResponse, "data"> { | ||
| data: { hash?: Hex; error?: string }; | ||
| } | ||
| export interface SignTypedDataResponse | ||
| extends Omit<GeminiSdkMessageResponse, "data"> { | ||
| export interface SignTypedDataResponse extends Omit<GeminiSdkMessageResponse, "data"> { | ||
| data: { hash?: Hex; error?: string }; | ||
| } | ||
| export interface SwitchChainResponse | ||
| extends Omit<GeminiSdkMessageResponse, "data"> { | ||
| export interface SwitchChainResponse extends Omit<GeminiSdkMessageResponse, "data"> { | ||
| data: { chainId?: number; error?: string }; | ||
| } | ||
| export interface GeminiSdkSendTransaction | ||
| extends Omit<GeminiSdkMessage, "data"> { | ||
| export interface GeminiSdkSendTransaction extends Omit<GeminiSdkMessage, "data"> { | ||
| data: TransactionRequest; | ||
@@ -162,2 +166,6 @@ } | ||
| export interface GeminiSdkSendBatchCalls extends Omit<GeminiSdkMessage, "data"> { | ||
| data: SendCallsParams; | ||
| } | ||
| export interface GeminiSdkSwitchChain extends Omit<GeminiSdkMessage, "data"> { | ||
@@ -167,4 +175,3 @@ data: number; | ||
| export interface GeminiSdkAppContextMessage | ||
| extends Omit<GeminiSdkMessage, "data"> { | ||
| export interface GeminiSdkAppContextMessage extends Omit<GeminiSdkMessage, "data"> { | ||
| data: AppContext; | ||
@@ -177,1 +184,69 @@ } | ||
| } | ||
| // EIP-5792 Types | ||
| export interface Call { | ||
| to: Address; | ||
| value?: Hex; | ||
| data?: Hex; | ||
| chainId?: Hex; | ||
| } | ||
| export interface SendCallsParams { | ||
| version: string; | ||
| chainId: Hex; | ||
| from: Address; | ||
| calls: Call[]; | ||
| capabilities?: Record<string, any>; | ||
| } | ||
| export interface WalletCapabilities { | ||
| [chainId: string]: { | ||
| atomic?: { | ||
| status: "supported" | "unsupported"; | ||
| }; | ||
| paymasterService?: { | ||
| supported: boolean; | ||
| }; | ||
| }; | ||
| } | ||
| export interface CallBatchMetadata { | ||
| id: string; | ||
| chainId: string; | ||
| from: Address; | ||
| calls: Call[]; | ||
| transactionHash?: Hex; | ||
| status: "pending" | "confirmed" | "failed" | "reverted"; | ||
| timestamp: number; | ||
| capabilities?: Record<string, any>; | ||
| } | ||
| export interface GetCallsStatusResponse { | ||
| version: string; | ||
| id: string; | ||
| chainId: Hex; | ||
| status: 100 | 200 | 400 | 500; // pending, confirmed, offchain failure, reverted | ||
| atomic: boolean; | ||
| receipts?: Array<{ | ||
| logs: Array<{ | ||
| address: Address; | ||
| data: Hex; | ||
| topics: Hex[]; | ||
| }>; | ||
| status: "success" | "reverted"; | ||
| blockHash: Hex; | ||
| blockNumber: Hex; | ||
| gasUsed: Hex; | ||
| transactionHash: Hex; | ||
| }>; | ||
| } | ||
| export interface SendCallsResponse { | ||
| id: string; | ||
| capabilities?: { | ||
| caip345?: { | ||
| caip2: string; | ||
| transactionHashes: Hex[]; | ||
| }; | ||
| }; | ||
| } |
| import { describe, expect, it } from "bun:test"; | ||
| import { | ||
| base64ToHex, | ||
| bufferToBase64URLString, | ||
| decodeBase64, | ||
| encodeBase64, | ||
| utf8StringToBuffer, | ||
| } from "./base64"; | ||
| import { base64ToHex, bufferToBase64URLString, decodeBase64, encodeBase64, utf8StringToBuffer } from "./base64"; | ||
@@ -200,3 +194,3 @@ describe("Base64 utilities", () => { | ||
| expect(result.length).toBe(10000); | ||
| expect(Array.from(result).every((b) => b === 97)).toBe(true); // 'a' = 97 in ASCII | ||
| expect(Array.from(result).every(b => b === 97)).toBe(true); // 'a' = 97 in ASCII | ||
| }); | ||
@@ -267,5 +261,3 @@ }); | ||
| const original = "48656c6c6f"; // "Hello" in hex | ||
| const bytes = new Uint8Array( | ||
| original.match(/.{2}/g)!.map((byte) => parseInt(byte, 16)), | ||
| ); | ||
| const bytes = new Uint8Array(original.match(/.{2}/g)!.map(byte => parseInt(byte, 16))); | ||
| const base64 = encodeBase64(bytes); | ||
@@ -296,10 +288,3 @@ const resultHex = base64ToHex(base64); | ||
| it("should maintain string data through utf8/base64 conversion", () => { | ||
| const testStrings = [ | ||
| "", | ||
| "Hello", | ||
| "Hello World!", | ||
| "Special chars: @#$%^&*()", | ||
| "UTF-8: 你好世界", | ||
| "Emoji: 😀🎉🚀", | ||
| ]; | ||
| const testStrings = ["", "Hello", "Hello World!", "Special chars: @#$%^&*()", "UTF-8: 你好世界", "Emoji: 😀🎉🚀"]; | ||
@@ -327,5 +312,3 @@ for (const original of testStrings) { | ||
| // Convert hex back to bytes | ||
| const bytes = new Uint8Array( | ||
| hex.match(/.{2}/g)!.map((byte) => parseInt(byte, 16)), | ||
| ); | ||
| const bytes = new Uint8Array(hex.match(/.{2}/g)!.map(byte => parseInt(byte, 16))); | ||
| expect(Array.from(bytes)).toEqual(Array.from(original)); | ||
@@ -332,0 +315,0 @@ } |
@@ -22,3 +22,3 @@ /** | ||
| Array.from(array) | ||
| .map((b) => String.fromCharCode(b)) | ||
| .map(b => String.fromCharCode(b)) | ||
| .join(""), | ||
@@ -66,5 +66,3 @@ ); | ||
| */ | ||
| export function bufferToBase64URLString( | ||
| buffer: ArrayBuffer | Uint8Array, | ||
| ): string { | ||
| export function bufferToBase64URLString(buffer: ArrayBuffer | Uint8Array): string { | ||
| const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer); | ||
@@ -104,4 +102,4 @@ return encodeBase64(bytes); | ||
| return Array.from(bytes) | ||
| .map((b) => b.toString(16).padStart(2, "0")) | ||
| .map(b => b.toString(16).padStart(2, "0")) | ||
| .join(""); | ||
| } |
@@ -14,4 +14,3 @@ import { describe, expect, test } from "bun:test"; | ||
| "0x900fb1e17b7766916a8dad6f8a26b3dbc4fe4f9b1ea5f2d20b7cb31e44c5ff54e63df1865b444a4e7b74a33ef8e3a269f77a6ba5afd072fc641ad5c7f9d626c7" as const; | ||
| const credentialId = | ||
| "XJ980eHLIRtTop-iX4-wAtSUQ-GxPv_6JIprPE2nN-RBgfJKZPWEWzC-amiRxzfjpks_7q7A8Q"; | ||
| const credentialId = "XJ980eHLIRtTop-iX4-wAtSUQ-GxPv_6JIprPE2nN-RBgfJKZPWEWzC-amiRxzfjpks_7q7A8Q"; | ||
@@ -31,4 +30,3 @@ const calculatedAddress = calculateWalletAddress({ | ||
| "0x900fb1e17b7766916a8dad6f8a26b3dbc4fe4f9b1ea5f2d20b7cb31e44c5ff54e63df1865b444a4e7b74a33ef8e3a269f77a6ba5afd072fc641ad5c7f9d626c7" as const; | ||
| const credentialId = | ||
| "XJ980eHLIRtTop-iX4-wAtSUQ-GxPv_6JIprPE2nN-RBgfJKZPWEWzC-amiRxzfjpks_7q7A8Q"; | ||
| const credentialId = "XJ980eHLIRtTop-iX4-wAtSUQ-GxPv_6JIprPE2nN-RBgfJKZPWEWzC-amiRxzfjpks_7q7A8Q"; | ||
@@ -64,8 +62,6 @@ const calculatedAddress = calculateV1Address({ | ||
| test("should generate correct authenticator ID hash", () => { | ||
| const credentialId = | ||
| "XJ980eHLIRtTop-iX4-wAtSUQ-GxPv_6JIprPE2nN-RBgfJKZPWEWzC-amiRxzfjpks_7q7A8Q"; | ||
| const credentialId = "XJ980eHLIRtTop-iX4-wAtSUQ-GxPv_6JIprPE2nN-RBgfJKZPWEWzC-amiRxzfjpks_7q7A8Q"; | ||
| const hash = generateAuthenticatorIdHash(credentialId); | ||
| const expectedHash = | ||
| "0xa919a485eff73c853844904a444f102f42d302320d3fee7c64136b0f4ef8357c"; | ||
| const expectedHash = "0xa919a485eff73c853844904a444f102f42d302320d3fee7c64136b0f4ef8357c"; | ||
@@ -84,5 +80,3 @@ console.log("Generated hash:", hash); | ||
| }); | ||
| }).toThrow( | ||
| "Invalid public key: must be 64-byte hex string (0x + 128 chars)", | ||
| ); | ||
| }).toThrow("Invalid public key: must be 64-byte hex string (0x + 128 chars)"); | ||
| }); | ||
@@ -93,4 +87,3 @@ | ||
| "0x900fb1e17b7766916a8dad6f8a26b3dbc4fe4f9b1ea5f2d20b7cb31e44c5ff54e63df1865b444a4e7b74a33ef8e3a269f77a6ba5afd072fc641ad5c7f9d626c7" as const; | ||
| const credentialId = | ||
| "XJ980eHLIRtTop-iX4-wAtSUQ-GxPv_6JIprPE2nN-RBgfJKZPWEWzC-amiRxzfjpks_7q7A8Q"; | ||
| const credentialId = "XJ980eHLIRtTop-iX4-wAtSUQ-GxPv_6JIprPE2nN-RBgfJKZPWEWzC-amiRxzfjpks_7q7A8Q"; | ||
@@ -97,0 +90,0 @@ const address1 = calculateWalletAddress({ credentialId, publicKey }); |
@@ -52,5 +52,3 @@ import { | ||
| params: CalculateWalletAddressParams, | ||
| contractAddresses: | ||
| | typeof V1_CONTRACT_ADDRESSES | ||
| | typeof V2_CONTRACT_ADDRESSES, | ||
| contractAddresses: typeof V1_CONTRACT_ADDRESSES | typeof V2_CONTRACT_ADDRESSES, | ||
| ): Address { | ||
@@ -61,5 +59,3 @@ const { publicKey, credentialId, index = 0n } = params; | ||
| if (!publicKey.startsWith("0x") || publicKey.length !== 130) { | ||
| throw new Error( | ||
| "Invalid public key: must be 64-byte hex string (0x + 128 chars)", | ||
| ); | ||
| throw new Error("Invalid public key: must be 64-byte hex string (0x + 128 chars)"); | ||
| } | ||
@@ -79,5 +75,3 @@ | ||
| if (!validateWebAuthnKey(webAuthnData)) { | ||
| throw new Error( | ||
| "Invalid WebAuthn key: coordinates are not on secp256r1 curve", | ||
| ); | ||
| throw new Error("Invalid WebAuthn key: coordinates are not on secp256r1 curve"); | ||
| } | ||
@@ -101,5 +95,3 @@ | ||
| */ | ||
| export function calculateWalletAddress( | ||
| params: CalculateWalletAddressParams, | ||
| ): Address { | ||
| export function calculateWalletAddress(params: CalculateWalletAddressParams): Address { | ||
| return processWalletAddressParams(params, V2_CONTRACT_ADDRESSES); | ||
@@ -112,5 +104,3 @@ } | ||
| */ | ||
| export function calculateV1Address( | ||
| params: CalculateWalletAddressParams, | ||
| ): Address { | ||
| export function calculateV1Address(params: CalculateWalletAddressParams): Address { | ||
| return processWalletAddressParams(params, V1_CONTRACT_ADDRESSES); | ||
@@ -140,9 +130,5 @@ } | ||
| */ | ||
| export function validateWebAuthnKey( | ||
| webAuthnData: WebAuthnValidatorData, | ||
| ): boolean { | ||
| const SECP256R1_P = | ||
| 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffffn; | ||
| const SECP256R1_B = | ||
| 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604bn; | ||
| export function validateWebAuthnKey(webAuthnData: WebAuthnValidatorData): boolean { | ||
| const SECP256R1_P = 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffffn; | ||
| const SECP256R1_B = 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604bn; | ||
@@ -152,8 +138,3 @@ const { pubKeyX, pubKeyY } = webAuthnData; | ||
| // Check if coordinates are valid | ||
| if ( | ||
| pubKeyX === 0n || | ||
| pubKeyY === 0n || | ||
| pubKeyX >= SECP256R1_P || | ||
| pubKeyY >= SECP256R1_P | ||
| ) { | ||
| if (pubKeyX === 0n || pubKeyY === 0n || pubKeyX >= SECP256R1_P || pubKeyY >= SECP256R1_P) { | ||
| return false; | ||
@@ -178,8 +159,5 @@ } | ||
| index: bigint; | ||
| contractAddresses: | ||
| | typeof V1_CONTRACT_ADDRESSES | ||
| | typeof V2_CONTRACT_ADDRESSES; | ||
| contractAddresses: typeof V1_CONTRACT_ADDRESSES | typeof V2_CONTRACT_ADDRESSES; | ||
| }): Address { | ||
| const { webAuthnData, authenticatorIdHash, index, contractAddresses } = | ||
| params; | ||
| const { webAuthnData, authenticatorIdHash, index, contractAddresses } = params; | ||
@@ -250,14 +228,6 @@ // Use provided contract addresses | ||
| // Format initialization data as expected by ProxyLib | ||
| const initData = encodeAbiParameters( | ||
| [{ type: "address" }, { type: "bytes" }], | ||
| [bootstrapper, bootstrapCall], | ||
| ); | ||
| const initData = encodeAbiParameters([{ type: "address" }, { type: "bytes" }], [bootstrapper, bootstrapCall]); | ||
| // Calculate CREATE2 address using the same logic as ProxyLib.predictProxyAddress | ||
| return predictProxyAddress( | ||
| accountImplementation, | ||
| salt, | ||
| initData, | ||
| factoryAddress, | ||
| ); | ||
| return predictProxyAddress(accountImplementation, salt, initData, factoryAddress); | ||
| } | ||
@@ -269,8 +239,3 @@ | ||
| */ | ||
| function predictProxyAddress( | ||
| implementation: Address, | ||
| salt: Hex, | ||
| initData: Hex, | ||
| deployer: Address, | ||
| ): Address { | ||
| function predictProxyAddress(implementation: Address, salt: Hex, initData: Hex, deployer: Address): Address { | ||
| // Encode the call to INexus.initializeAccount with initData | ||
@@ -299,5 +264,3 @@ const initializeCall = encodeFunctionData({ | ||
| const initCodeHash = keccak256( | ||
| encodePacked(["bytes", "bytes"], [nexusProxyCreationCode, constructorArgs]), | ||
| ); | ||
| const initCodeHash = keccak256(encodePacked(["bytes", "bytes"], [nexusProxyCreationCode, constructorArgs])); | ||
@@ -304,0 +267,0 @@ // Standard CREATE2 formula |
@@ -76,4 +76,3 @@ import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test"; | ||
| // Using a random address that shouldn't have an ENS name | ||
| const randomAddress: Address = | ||
| "0x1234567890123456789012345678901234567890"; | ||
| const randomAddress: Address = "0x1234567890123456789012345678901234567890"; | ||
| const result = await reverseResolveEns(randomAddress); | ||
@@ -118,5 +117,3 @@ | ||
| }); | ||
| expect(mockFetch).toHaveBeenCalledWith( | ||
| `${ENS_API_URL}/reverse/${testAddress}`, | ||
| ); | ||
| expect(mockFetch).toHaveBeenCalledWith(`${ENS_API_URL}/reverse/${testAddress}`); | ||
| }); | ||
@@ -263,7 +260,5 @@ | ||
| expect(mockFetch).toHaveBeenCalledWith(expectedUrl); | ||
| expect(expectedUrl).toBe( | ||
| `https://horizon-api.gemini.com/api/ens/reverse/${testAddress}`, | ||
| ); | ||
| expect(expectedUrl).toBe(`https://horizon-api.gemini.com/api/ens/reverse/${testAddress}`); | ||
| }); | ||
| }); | ||
| }); |
+2
-6
@@ -6,5 +6,3 @@ import type { Address } from "viem"; | ||
| export async function reverseResolveEns( | ||
| address: Address, | ||
| ): Promise<ReverseEnsResponse> { | ||
| export async function reverseResolveEns(address: Address): Promise<ReverseEnsResponse> { | ||
| try { | ||
@@ -14,5 +12,3 @@ const response = await fetch(`${ENS_API_URL}/reverse/${address}`); | ||
| if (!response.ok) { | ||
| throw new Error( | ||
| `ENS API request failed: ${response.status} ${response.statusText}`, | ||
| ); | ||
| throw new Error(`ENS API request failed: ${response.status} ${response.statusText}`); | ||
| } | ||
@@ -19,0 +15,0 @@ |
| export { SDK_BACKEND_URL, SDK_VERSION } from "../constants"; | ||
| export { base64ToHex, bufferToBase64URLString, decodeBase64, encodeBase64, utf8StringToBuffer } from "./base64"; | ||
| export { | ||
| base64ToHex, | ||
| bufferToBase64URLString, | ||
| decodeBase64, | ||
| encodeBase64, | ||
| utf8StringToBuffer, | ||
| } from "./base64"; | ||
| export { | ||
| calculateV1Address, | ||
@@ -11,0 +5,0 @@ calculateWalletAddress, |
@@ -11,7 +11,3 @@ import { rpcErrors } from "@metamask/rpc-errors"; | ||
| const popupId = `wallet_${window?.crypto?.randomUUID()}`; | ||
| const popup = window.open( | ||
| url, | ||
| popupId, | ||
| `width=${POPUP_WIDTH}, height=${POPUP_HEIGHT}, left=${left}, top=${top}`, | ||
| ); | ||
| const popup = window.open(url, popupId, `width=${POPUP_WIDTH}, height=${POPUP_HEIGHT}, left=${left}, top=${top}`); | ||
@@ -18,0 +14,0 @@ popup?.focus(); |
@@ -27,8 +27,4 @@ import { hexStringFromNumber } from "./index"; | ||
| expect(hexStringFromNumber(0)).toBe("0x0"); // Zero case | ||
| expect(hexStringFromNumber(Number.MAX_SAFE_INTEGER)).toBe( | ||
| `0x${BigInt(Number.MAX_SAFE_INTEGER).toString(16)}`, | ||
| ); | ||
| expect(hexStringFromNumber(Number.MIN_SAFE_INTEGER)).toBe( | ||
| `0x${BigInt(Number.MIN_SAFE_INTEGER).toString(16)}`, | ||
| ); | ||
| expect(hexStringFromNumber(Number.MAX_SAFE_INTEGER)).toBe(`0x${BigInt(Number.MAX_SAFE_INTEGER).toString(16)}`); | ||
| expect(hexStringFromNumber(Number.MIN_SAFE_INTEGER)).toBe(`0x${BigInt(Number.MIN_SAFE_INTEGER).toString(16)}`); | ||
| }); | ||
@@ -35,0 +31,0 @@ |
@@ -6,6 +6,2 @@ export const hexStringFromNumber = (num: number): string => { | ||
| export const safeJsonStringify = (obj: any) => | ||
| JSON.stringify( | ||
| obj, | ||
| (_, value) => (typeof value === "bigint" ? value.toString() + "n" : value), | ||
| 2, | ||
| ); | ||
| JSON.stringify(obj, (_, value) => (typeof value === "bigint" ? value.toString() + "n" : value), 2); |
| import { beforeEach, describe, expect, it, mock } from "bun:test"; | ||
| import { type Address } from "viem"; | ||
| import { DEFAULT_CHAIN_ID, getDefaultRpcUrl, SUPPORTED_CHAIN_IDS } from "../constants"; | ||
| import { GeminiStorage, STORAGE_ETH_ACCOUNTS_KEY, STORAGE_ETH_ACTIVE_CHAIN_KEY } from "../storage"; | ||
| import { | ||
| DEFAULT_CHAIN_ID, | ||
| getDefaultRpcUrl, | ||
| SUPPORTED_CHAIN_IDS, | ||
| } from "../constants"; | ||
| import { | ||
| GeminiStorage, | ||
| STORAGE_ETH_ACCOUNTS_KEY, | ||
| STORAGE_ETH_ACTIVE_CHAIN_KEY, | ||
| } from "../storage"; | ||
| import { | ||
| type Chain, | ||
@@ -26,6 +18,4 @@ type ConnectResponse, | ||
| const mockAddress = "0xAfEDA61dB9e162293b2eF2C2bC5A800b37Bb5E4a" as Address; | ||
| const mockTxHash = | ||
| "0x5de3752c591ecc35d1046f3aca2eba1ba5bdcfb786639a8661e9ecb823675743"; | ||
| const mockSigHash = | ||
| "0x020d671b80fbd20466d8cb65cef79a24e3bca3fdf82e9dd89d78e7a4c4c045b"; | ||
| const mockTxHash = "0x5de3752c591ecc35d1046f3aca2eba1ba5bdcfb786639a8661e9ecb823675743"; | ||
| const mockSigHash = "0x020d671b80fbd20466d8cb65cef79a24e3bca3fdf82e9dd89d78e7a4c4c045b"; | ||
@@ -57,3 +47,3 @@ // Set up global window mock for this test file only | ||
| it("should return true for all supported chains", () => { | ||
| SUPPORTED_CHAIN_IDS.forEach((chainId) => { | ||
| SUPPORTED_CHAIN_IDS.forEach(chainId => { | ||
| expect(isChainSupportedByGeminiSw(chainId)).toBe(true); | ||
@@ -117,6 +107,3 @@ }); | ||
| expect(mockStorage.storeObject).toHaveBeenCalledWith( | ||
| STORAGE_ETH_ACCOUNTS_KEY, | ||
| accounts, | ||
| ); | ||
| expect(mockStorage.storeObject).toHaveBeenCalledWith(STORAGE_ETH_ACCOUNTS_KEY, accounts); | ||
| }); | ||
@@ -129,18 +116,9 @@ | ||
| expect(mockStorage.storeObject).toHaveBeenCalledWith( | ||
| STORAGE_ETH_ACTIVE_CHAIN_KEY, | ||
| chain, | ||
| ); | ||
| expect(mockStorage.storeObject).toHaveBeenCalledWith(STORAGE_ETH_ACTIVE_CHAIN_KEY, chain); | ||
| }); | ||
| it("should load accounts from storage", async () => { | ||
| const accounts = await mockStorage.loadObject( | ||
| STORAGE_ETH_ACCOUNTS_KEY, | ||
| [], | ||
| ); | ||
| const accounts = await mockStorage.loadObject(STORAGE_ETH_ACCOUNTS_KEY, []); | ||
| expect(mockStorage.loadObject).toHaveBeenCalledWith( | ||
| STORAGE_ETH_ACCOUNTS_KEY, | ||
| [], | ||
| ); | ||
| expect(mockStorage.loadObject).toHaveBeenCalledWith(STORAGE_ETH_ACCOUNTS_KEY, []); | ||
| expect(accounts).toEqual([]); | ||
@@ -154,6 +132,3 @@ }); | ||
| expect(mockStorage.loadObject).toHaveBeenCalledWith( | ||
| STORAGE_ETH_ACTIVE_CHAIN_KEY, | ||
| { id: DEFAULT_CHAIN_ID }, | ||
| ); | ||
| expect(mockStorage.loadObject).toHaveBeenCalledWith(STORAGE_ETH_ACTIVE_CHAIN_KEY, { id: DEFAULT_CHAIN_ID }); | ||
| expect(chain.id).toBe(DEFAULT_CHAIN_ID); | ||
@@ -160,0 +135,0 @@ }); |
+224
-52
| import { | ||
| type Address, | ||
| type Hex, | ||
| type SignMessageParameters, | ||
@@ -10,10 +11,7 @@ type SignTypedDataParameters, | ||
| import { Communicator } from "../communicator"; | ||
| import { DEFAULT_CHAIN_ID, getDefaultRpcUrl, SUPPORTED_CHAIN_IDS } from "../constants"; | ||
| import { | ||
| DEFAULT_CHAIN_ID, | ||
| getDefaultRpcUrl, | ||
| SUPPORTED_CHAIN_IDS, | ||
| } from "../constants"; | ||
| import { | ||
| GeminiStorage, | ||
| type IStorage, | ||
| STORAGE_CALL_BATCHES_KEY, | ||
| STORAGE_ETH_ACCOUNTS_KEY, | ||
@@ -23,2 +21,3 @@ STORAGE_ETH_ACTIVE_CHAIN_KEY, | ||
| import { | ||
| type CallBatchMetadata, | ||
| type Chain, | ||
@@ -33,2 +32,5 @@ type ConnectResponse, | ||
| type GeminiSdkSignTypedData, | ||
| type GetCallsStatusResponse, | ||
| type SendCallsParams, | ||
| type SendCallsResponse, | ||
| type SendTransactionResponse, | ||
@@ -38,8 +40,8 @@ type SignMessageResponse, | ||
| type SwitchChainResponse, | ||
| type WalletCapabilities, | ||
| } from "../types"; | ||
| import { hexStringFromNumber } from "../utils"; | ||
| export function isChainSupportedByGeminiSw(chainId: number): boolean { | ||
| return SUPPORTED_CHAIN_IDS.includes( | ||
| chainId as (typeof SUPPORTED_CHAIN_IDS)[number], | ||
| ); | ||
| return SUPPORTED_CHAIN_IDS.includes(chainId as (typeof SUPPORTED_CHAIN_IDS)[number]); | ||
| } | ||
@@ -54,8 +56,3 @@ | ||
| constructor({ | ||
| appMetadata, | ||
| chain, | ||
| onDisconnectCallback, | ||
| storage, | ||
| }: Readonly<GeminiProviderConfig>) { | ||
| constructor({ appMetadata, chain, onDisconnectCallback, storage }: Readonly<GeminiProviderConfig>) { | ||
| this.communicator = new Communicator({ | ||
@@ -84,10 +81,4 @@ appMetadata, | ||
| const [storedChain, storedAccounts] = await Promise.all([ | ||
| this.storage.loadObject<Chain>( | ||
| STORAGE_ETH_ACTIVE_CHAIN_KEY, | ||
| fallbackChain, | ||
| ), | ||
| this.storage.loadObject<Address[]>( | ||
| STORAGE_ETH_ACCOUNTS_KEY, | ||
| this.accounts, | ||
| ), | ||
| this.storage.loadObject<Chain>(STORAGE_ETH_ACTIVE_CHAIN_KEY, fallbackChain), | ||
| this.storage.loadObject<Address[]>(STORAGE_ETH_ACCOUNTS_KEY, this.accounts), | ||
| ]); | ||
@@ -109,6 +100,3 @@ | ||
| await this.ensureInitialized(); | ||
| const response = await this.sendMessageToPopup< | ||
| GeminiSdkMessage, | ||
| ConnectResponse | ||
| >({ | ||
| const response = await this.sendMessageToPopup<GeminiSdkMessage, ConnectResponse>({ | ||
| chainId: this.chain.id, | ||
@@ -146,6 +134,3 @@ event: GeminiSdkEvent.SDK_CONNECT, | ||
| // Message sdk to inform user of error | ||
| const response = await this.sendMessageToPopup< | ||
| GeminiSdkMessage, | ||
| SwitchChainResponse | ||
| >({ | ||
| const response = await this.sendMessageToPopup<GeminiSdkMessage, SwitchChainResponse>({ | ||
| chainId: this.chain.id, | ||
@@ -161,10 +146,5 @@ data: id, | ||
| async sendTransaction( | ||
| txData: TransactionRequest, | ||
| ): Promise<SendTransactionResponse["data"]> { | ||
| async sendTransaction(txData: TransactionRequest): Promise<SendTransactionResponse["data"]> { | ||
| await this.ensureInitialized(); | ||
| const response = await this.sendMessageToPopup< | ||
| GeminiSdkSendTransaction, | ||
| SendTransactionResponse | ||
| >({ | ||
| const response = await this.sendMessageToPopup<GeminiSdkSendTransaction, SendTransactionResponse>({ | ||
| chainId: this.chain.id, | ||
@@ -179,10 +159,5 @@ data: txData, | ||
| async signData({ | ||
| message, | ||
| }: SignMessageParameters): Promise<SignMessageResponse["data"]> { | ||
| async signData({ message }: SignMessageParameters): Promise<SignMessageResponse["data"]> { | ||
| await this.ensureInitialized(); | ||
| const response = await this.sendMessageToPopup< | ||
| GeminiSdkSignMessage, | ||
| SignMessageResponse | ||
| >({ | ||
| const response = await this.sendMessageToPopup<GeminiSdkSignMessage, SignMessageResponse>({ | ||
| chainId: this.chain.id, | ||
@@ -204,6 +179,3 @@ data: { message }, | ||
| await this.ensureInitialized(); | ||
| const response = await this.sendMessageToPopup< | ||
| GeminiSdkSignTypedData, | ||
| SignTypedDataResponse | ||
| >({ | ||
| const response = await this.sendMessageToPopup<GeminiSdkSignTypedData, SignTypedDataResponse>({ | ||
| chainId: this.chain.id, | ||
@@ -232,6 +204,206 @@ data: { | ||
| private sendMessageToPopup< | ||
| M extends GeminiSdkMessage, | ||
| R extends GeminiSdkMessageResponse, | ||
| >(request: GeminiSdkMessage): Promise<R> { | ||
| // EIP-5792 Wallet Call API Methods | ||
| getCapabilities(requestedChainIds?: string[]): WalletCapabilities { | ||
| const capabilities: WalletCapabilities = {}; | ||
| const chainIds = requestedChainIds?.map(id => parseInt(id, 16)) || [this.chain.id]; | ||
| for (const chainId of chainIds) { | ||
| const chainIdHex = hexStringFromNumber(chainId); | ||
| capabilities[chainIdHex] = { | ||
| atomic: { | ||
| status: "supported", // Smart accounts support atomic batch execution | ||
| }, | ||
| paymasterService: { | ||
| supported: true, | ||
| }, | ||
| }; | ||
| } | ||
| return capabilities; | ||
| } | ||
| async sendCalls(params: SendCallsParams): Promise<SendCallsResponse> { | ||
| await this.ensureInitialized(); | ||
| // Generate unique bundle ID | ||
| const batchId = window?.crypto?.randomUUID() || `batch-${Date.now()}-${Math.random()}`; | ||
| // Validate chain ID matches current chain | ||
| const requestedChainId = parseInt(params.chainId, 16); | ||
| if (requestedChainId !== this.chain.id) { | ||
| throw new Error(`Chain mismatch. Expected ${this.chain.id}, got ${requestedChainId}`); | ||
| } | ||
| // Validate we have calls | ||
| if (!params.calls || params.calls.length === 0) { | ||
| throw new Error("No calls provided"); | ||
| } | ||
| // Create batch metadata | ||
| const batchMetadata: CallBatchMetadata = { | ||
| calls: params.calls, | ||
| capabilities: params.capabilities, | ||
| chainId: params.chainId, | ||
| from: params.from, | ||
| id: batchId, | ||
| status: "pending", | ||
| timestamp: Date.now(), | ||
| }; | ||
| // Store batch metadata for status tracking | ||
| const batches = await this.storage.loadObject<Record<string, CallBatchMetadata>>(STORAGE_CALL_BATCHES_KEY, {}); | ||
| batches[batchId] = batchMetadata; | ||
| await this.storage.storeObject(STORAGE_CALL_BATCHES_KEY, batches); | ||
| try { | ||
| // Send the batch call through the popup/iframe | ||
| // The wallet-web will handle this through the smart account client | ||
| const response = await this.sendMessageToPopup<GeminiSdkMessage, SendTransactionResponse>({ | ||
| chainId: this.chain.id, | ||
| data: { | ||
| calls: params.calls, | ||
| }, | ||
| event: GeminiSdkEvent.SDK_SEND_BATCH_CALLS, | ||
| origin: window.location.origin, | ||
| }); | ||
| if (response.data.error) { | ||
| throw new Error(response.data.error); | ||
| } | ||
| // Update batch with transaction hash | ||
| batchMetadata.transactionHash = response.data.hash as Hex; | ||
| batchMetadata.status = "pending"; | ||
| batches[batchId] = batchMetadata; | ||
| await this.storage.storeObject(STORAGE_CALL_BATCHES_KEY, batches); | ||
| // Return response with bundle ID and transaction hash | ||
| return { | ||
| capabilities: { | ||
| caip345: { | ||
| caip2: `eip155:${requestedChainId}`, | ||
| transactionHashes: [response.data.hash as Hex], | ||
| }, | ||
| }, | ||
| id: batchId, | ||
| }; | ||
| } catch (error) { | ||
| // Mark batch as failed | ||
| batchMetadata.status = "failed"; | ||
| batches[batchId] = batchMetadata; | ||
| await this.storage.storeObject(STORAGE_CALL_BATCHES_KEY, batches); | ||
| throw error; | ||
| } | ||
| } | ||
| async getCallsStatus(batchId: string): Promise<GetCallsStatusResponse> { | ||
| await this.ensureInitialized(); | ||
| const batches = await this.storage.loadObject<Record<string, CallBatchMetadata>>(STORAGE_CALL_BATCHES_KEY, {}); | ||
| const batch = batches[batchId]; | ||
| if (!batch) { | ||
| throw new Error(`Unknown bundle ID: ${batchId}`); | ||
| } | ||
| // If we have a transaction hash, check its status on chain | ||
| if (batch.transactionHash && this.chain.rpcUrl) { | ||
| try { | ||
| const response = await fetch(this.chain.rpcUrl, { | ||
| body: JSON.stringify({ | ||
| id: 1, | ||
| jsonrpc: "2.0", | ||
| method: "eth_getTransactionReceipt", | ||
| params: [batch.transactionHash], | ||
| }), | ||
| headers: { "Content-Type": "application/json" }, | ||
| method: "POST", | ||
| }); | ||
| const json = await response.json(); | ||
| const receipt = json.result; | ||
| if (receipt) { | ||
| // Update batch status based on receipt | ||
| const receiptStatus = receipt.status === "0x1" ? "confirmed" : "reverted"; | ||
| batch.status = receiptStatus; | ||
| batches[batchId] = batch; | ||
| await this.storage.storeObject(STORAGE_CALL_BATCHES_KEY, batches); | ||
| return { | ||
| atomic: true, | ||
| chainId: batch.chainId as Hex, | ||
| id: batchId, | ||
| receipts: [ | ||
| { | ||
| blockHash: receipt.blockHash, | ||
| blockNumber: receipt.blockNumber, | ||
| gasUsed: receipt.gasUsed, | ||
| logs: receipt.logs.map((log: { address: string; data: string; topics: string[] }) => ({ | ||
| address: log.address, | ||
| data: log.data, | ||
| topics: log.topics, | ||
| })), | ||
| status: receiptStatus === "confirmed" ? "success" : "reverted", | ||
| transactionHash: receipt.transactionHash, | ||
| }, | ||
| ], | ||
| status: receiptStatus === "confirmed" ? 200 : 500, | ||
| version: "2.0.0", | ||
| }; | ||
| } | ||
| } catch (error) { | ||
| // If receipt fetch fails, return pending status | ||
| console.error("Failed to fetch transaction receipt:", error); | ||
| } | ||
| } | ||
| // Return status based on batch metadata | ||
| let statusCode: 100 | 200 | 400 | 500; | ||
| switch (batch.status) { | ||
| case "pending": | ||
| statusCode = 100; | ||
| break; | ||
| case "confirmed": | ||
| statusCode = 200; | ||
| break; | ||
| case "failed": | ||
| statusCode = 400; | ||
| break; | ||
| case "reverted": | ||
| statusCode = 500; | ||
| break; | ||
| default: | ||
| statusCode = 100; | ||
| } | ||
| return { | ||
| atomic: true, | ||
| chainId: batch.chainId as Hex, | ||
| id: batchId, | ||
| status: statusCode, | ||
| version: "2.0.0", | ||
| }; | ||
| } | ||
| async showCallsStatus(batchId: string): Promise<void> { | ||
| await this.ensureInitialized(); | ||
| // Validate batch exists | ||
| const batches = await this.storage.loadObject<Record<string, CallBatchMetadata>>(STORAGE_CALL_BATCHES_KEY, {}); | ||
| const batch = batches[batchId]; | ||
| if (!batch) { | ||
| throw new Error(`Unknown bundle ID: ${batchId}`); | ||
| } | ||
| // Open SDK UI to show call status | ||
| // TODO: Implement actual UI showing via communicator | ||
| // For now, this just validates the batch exists | ||
| } | ||
| private sendMessageToPopup<M extends GeminiSdkMessage, R extends GeminiSdkMessageResponse>( | ||
| request: GeminiSdkMessage, | ||
| ): Promise<R> { | ||
| return this.communicator.postRequestAndWaitForResponse<M, R>({ | ||
@@ -238,0 +410,0 @@ ...request, |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
446620
14.47%19
-17.39%6719
6.4%378
1.07%25
13.64%