UNPKG

@gemini-wallet/core

Version:

Core SDK for Gemini Wallet integration with popup communication

222 lines (200 loc) 5.91 kB
import { type Address, type SignMessageParameters, type SignTypedDataParameters, type SwitchChainParameters, type TransactionRequest, } from "viem"; import { Communicator } from "@/communicator"; import { DEFAULT_CHAIN_ID, getDefaultRpcUrl, SUPPORTED_CHAIN_IDS, } from "@/constants"; import { GeminiStorage, type IStorage, STORAGE_ETH_ACCOUNTS_KEY, STORAGE_ETH_ACTIVE_CHAIN_KEY, } from "@/storage"; import { type Chain, type ConnectResponse, type GeminiProviderConfig, GeminiSdkEvent, type GeminiSdkMessage, type GeminiSdkMessageResponse, type GeminiSdkSendTransaction, type GeminiSdkSignMessage, type GeminiSdkSignTypedData, type SendTransactionResponse, type SignMessageResponse, type SignTypedDataResponse, type SwitchChainResponse, } from "@/types"; export function isChainSupportedByGeminiSw(chainId: number): boolean { return SUPPORTED_CHAIN_IDS.includes( chainId as (typeof SUPPORTED_CHAIN_IDS)[number], ); } export class GeminiWallet { private readonly communicator: Communicator; private readonly storage: IStorage; private initPromise: Promise<void>; public accounts: Address[] = []; public chain: Chain = { id: DEFAULT_CHAIN_ID }; constructor({ appMetadata, chain, onDisconnectCallback, storage, }: Readonly<GeminiProviderConfig>) { this.communicator = new Communicator({ appMetadata, onDisconnectCallback, }); // Use provided storage or create default GeminiStorage for web this.storage = storage || new GeminiStorage(); // Initialize storage data - use provided chain config or fallback to default const initialChain = chain || { id: DEFAULT_CHAIN_ID }; this.initPromise = this.initializeFromStorage(initialChain); } private async initializeFromStorage(defaultChain: Chain): Promise<void> { const fallbackChain: Chain = { id: defaultChain.id, rpcUrl: defaultChain.rpcUrl || getDefaultRpcUrl(defaultChain.id), }; 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, ), ]); // Ensure chain has rpcUrl fallback this.chain = { ...storedChain, rpcUrl: storedChain.rpcUrl || getDefaultRpcUrl(storedChain.id), }; this.accounts = storedAccounts; } private async ensureInitialized(): Promise<void> { await this.initPromise; } async connect(): Promise<Address[]> { await this.ensureInitialized(); const response = await this.sendMessageToPopup< GeminiSdkMessage, ConnectResponse >({ chainId: this.chain.id, event: GeminiSdkEvent.SDK_CONNECT, origin: window.location.origin, }); this.accounts = [response.data.address]; await this.storage.storeObject(STORAGE_ETH_ACCOUNTS_KEY, this.accounts); return this.accounts; } async switchChain({ id, }: SwitchChainParameters): Promise<string | undefined> { await this.ensureInitialized(); // If chain is supported return response immediately if (isChainSupportedByGeminiSw(id)) { this.chain = { id, rpcUrl: getDefaultRpcUrl(id), }; // Store new active chain with rpcUrl await this.storage.storeObject(STORAGE_ETH_ACTIVE_CHAIN_KEY, this.chain); // Per EIP-3326, must return null if chain switch was success return undefined; } // Message sdk to inform user of error const response = await this.sendMessageToPopup< GeminiSdkMessage, SwitchChainResponse >({ chainId: this.chain.id, data: id, event: GeminiSdkEvent.SDK_SWITCH_CHAIN, origin: window.location.origin, }); // Return error message return response.data.error!; } async sendTransaction( txData: TransactionRequest, ): Promise<SendTransactionResponse["data"]> { await this.ensureInitialized(); const response = await this.sendMessageToPopup< GeminiSdkSendTransaction, SendTransactionResponse >({ chainId: this.chain.id, data: txData, event: GeminiSdkEvent.SDK_SEND_TRANSACTION, origin: window.location.origin, }); return response.data; } async signData({ message, }: SignMessageParameters): Promise<SignMessageResponse["data"]> { await this.ensureInitialized(); const response = await this.sendMessageToPopup< GeminiSdkSignMessage, SignMessageResponse >({ chainId: this.chain.id, data: { message }, event: GeminiSdkEvent.SDK_SIGN_DATA, origin: window.location.origin, }); return response.data; } async signTypedData({ message, types, primaryType, domain, }: SignTypedDataParameters): Promise<SignTypedDataResponse["data"]> { await this.ensureInitialized(); const response = await this.sendMessageToPopup< GeminiSdkSignTypedData, SignTypedDataResponse >({ chainId: this.chain.id, data: { domain, message, primaryType, types, }, event: GeminiSdkEvent.SDK_SIGN_TYPED_DATA, origin: window.location.origin, }); return response.data; } async openSettings(): Promise<void> { await this.ensureInitialized(); await this.sendMessageToPopup<GeminiSdkMessage, GeminiSdkMessageResponse>({ chainId: this.chain.id, data: {}, event: GeminiSdkEvent.SDK_OPEN_SETTINGS, origin: window.location.origin, }); } private sendMessageToPopup< M extends GeminiSdkMessage, R extends GeminiSdkMessageResponse, >(request: GeminiSdkMessage): Promise<R> { return this.communicator.postRequestAndWaitForResponse<M, R>({ ...request, requestId: window?.crypto?.randomUUID(), }); } }