@gemini-wallet/core
Version:
Core SDK for Gemini Wallet integration with popup communication
222 lines (200 loc) • 5.91 kB
text/typescript
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(),
});
}
}