Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 3 additions & 18 deletions src/common/utils/WalletUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,28 +96,13 @@ describe("WalletUtils.getKeplr", () => {
vi.restoreAllMocks();
});

it("should return window.keplr when extension is already set and identifies as Keplr", async () => {
const fakeKeplr = { isKeplr: true, marker: "keplr-instance" };
it("should return window.keplr when extension is already set", async () => {
const fakeKeplr = { marker: "keplr-instance" };
w.keplr = fakeKeplr;
const result = await WalletUtils.getKeplr();
expect(result).toBe(fakeKeplr);
});

it("should return undefined when window.keplr is set but isKeplr !== true", async () => {
// Defends against wallet-aggregator placeholder injection: a hostile extension can
// populate window.keplr with a Keplr-shaped object lacking the identity flag.
w.keplr = { marker: "aggregator-shim" };
const result = await WalletUtils.getKeplr();
expect(result).toBeUndefined();
});

it("should return undefined when isKeplr is truthy-but-not-strictly-true", async () => {
// Strict equality `=== true` rejects truthy non-true values like 1 or "yes".
w.keplr = { isKeplr: 1, marker: "loose-keplr" };
const result = await WalletUtils.getKeplr();
expect(result).toBeUndefined();
});

it("should return the extension when document is complete and extension becomes available", async () => {
// No w.keplr set; document.readyState in jsdom is "complete" by default.
// The code path then does Promise.resolve(w[prop]) — returns undefined here.
Expand All @@ -142,7 +127,7 @@ describe("WalletUtils.getKeplr", () => {
configurable: true,
get: () => "complete"
});
const fakeKeplr = { isKeplr: true, marker: "delayed-keplr" };
const fakeKeplr = { marker: "delayed-keplr" };
w.keplr = fakeKeplr;
document.dispatchEvent(new Event("readystatechange"));

Expand Down
18 changes: 8 additions & 10 deletions src/common/utils/WalletUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,26 @@ import { WalletManager } from ".";
import { Wallet, NETWORK_DATA } from "@/networks";
import { fetchEndpoints } from "./EndpointService";

// Defense against extensions that inject a `window.keplr` shim without identifying as
// Keplr. Real Keplr always sets `isKeplr === true`. Strict-equality guard mirrors the
// `isPhantom`/`isSolflare` checks in `src/networks/sol/wallet.ts` (PR #155).
function isRealKeplr(k?: Keplr): k is Keplr {
return (k as unknown as { isKeplr?: unknown })?.isKeplr === true;
}

// Note: unlike Phantom/Solflare, Keplr does NOT expose an `isKeplr === true` marker
// on its Cosmos provider (`window.keplr`). The `isKeplr` flag in `@keplr-wallet/types`
// is on the EthereumProvider type — Keplr's EVM bridge — not on the main Keplr
// interface. A strict-equality marker check rejects real Keplr; do not re-add one
// without a verified, Cosmos-side identity signal. See `runbooks/webapp_wallet_network.md`.
function getKeplrExtension(): Promise<Keplr | undefined> {
const w = window as unknown as { keplr?: Keplr };

if (isRealKeplr(w.keplr)) {
if (w.keplr) {
return Promise.resolve(w.keplr);
}

if (document.readyState === "complete") {
return Promise.resolve(isRealKeplr(w.keplr) ? w.keplr : undefined);
return Promise.resolve(w.keplr);
}

return new Promise((resolve) => {
const documentStateChange = (event: Event) => {
if (event.target && (event.target as Document).readyState === "complete") {
resolve(isRealKeplr(w.keplr) ? w.keplr : undefined);
resolve(w.keplr);
document.removeEventListener("readystatechange", documentStateChange);
}
};
Expand Down
Loading