Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@gemini-wallet/core

Package Overview
Dependencies
Maintainers
3
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@gemini-wallet/core - npm Package Compare versions

Comparing version
0.3.1
to
0.3.2
+250
-89
dist/index.cjs

@@ -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,

@@ -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 };

@@ -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 };

@@ -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,

{
"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",

@@ -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 @@ });

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

@@ -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({

@@ -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

@@ -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(),

@@ -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";
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}`);
});
});
});

@@ -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 @@ });

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