The Arkena wallet injects window.arkena (and window.canton) into
every browser tab where the extension is enabled. Injection is
asynchronous, so detection has to handle two cases: the script ran before
your code, or your code ran first.
Synchronous case
If the wallet has already injected,
window.arkenais set. A one-liner is enough:Codeconst provider = typeof window !== "undefined" ? window.arkena : null;Server-side,
windowis undefined — always check first. SSR-rendered components need auseEffectboundary.Asynchronous case
If your code runs before the extension's content script, listen for the injection event. The wallet dispatches
arkena#initializedonce on injection (andcanton#initializedfor CIP-103 compatibility).Codefunction waitForArkena(timeoutMs = 1000): Promise<typeof window.arkena | null> { if (typeof window === "undefined") return Promise.resolve(null); if (window.arkena) return Promise.resolve(window.arkena); return new Promise((resolve) => { const onReady = () => resolve(window.arkena); window.addEventListener("arkena#initialized", onReady, { once: true }); setTimeout(() => resolve(window.arkena ?? null), timeoutMs); }); }CIP-103 multi-wallet discovery
The provider also follows CIP-103 (the EIP-6963 pattern): on every
canton:requestProviderevent from the page, the wallet dispatches acanton:announceProviderevent with its identity. Wallet-agnostic libraries use this to discover all installed Canton wallets without conflicting on the globalwindownamespace.Codeconst wallets: Array<{ uuid: string; name: string; provider: unknown }> = []; window.addEventListener("canton:announceProvider", (e: Event) => { const detail = (e as CustomEvent).detail; wallets.push({ uuid: detail.uuid, name: detail.name, provider: detail.provider }); }); window.dispatchEvent(new Event("canton:requestProvider"));When only Arkena is installed, the discovery surface and
window.arkenaresolve to the same object.
A reusable helper
Drop this into src/lib/arkena.ts (or wherever your dApp keeps its
provider helpers):
src/lib/arkena.ts
type ArkenaProvider = {
isArkena: true;
isCanton: true;
request: (args: { method: string; params?: Record<string, unknown> }) => Promise<unknown>;
on: (event: string, handler: (...args: unknown[]) => void) => void;
removeListener: (event: string, handler: (...args: unknown[]) => void) => void;
};
declare global {
interface Window {
arkena?: ArkenaProvider;
}
}
export async function getArkenaProvider(timeoutMs = 1000): Promise<ArkenaProvider | null> {
if (typeof window === "undefined") return null;
if (window.arkena) return window.arkena;
return new Promise((resolve) => {
const onReady = () => resolve(window.arkena ?? null);
window.addEventListener("arkena#initialized", onReady, { once: true });
setTimeout(() => resolve(window.arkena ?? null), timeoutMs);
});
}SSR boundary
In Next.js App Router, detection has to run on the client. Put the
client-only logic behind 'use client' and useEffect:
"use client";
import { useEffect, useState } from "react";
import { getArkenaProvider } from "@/lib/arkena";
export function ArkenaStatus() {
const [present, setPresent] = useState<boolean | null>(null);
useEffect(() => {
void getArkenaProvider().then((p) => setPresent(Boolean(p)));
}, []);
if (present === null) return null;
return present ? <span>Arkena detected</span> : <span>Install Arkena</span>;
}What's next
- Connect and disconnect — request access once you have a provider.
- Handle events — react to the
wallet's
accountsChangedanddisconnectevents.