Contents
Security and custody
The section that should be checkable rather than believed.
A wallet asks for more trust than any other kind of software. The response should not be a promise, it should be a set of properties anyone can verify.
Key generation
A wallet begins with entropy from the browser's cryptographic random source, and nothing else. No server is contacted, no identifier is created, and the recovery phrase exists only on the device that made it. It is shown to you once, and never leaves.
1export async function createWallet(password: string) {2const mnemonic = newMnemonic(); // BIP-39, browser CSPRNG3const { address } = deriveFromMnemonic(mnemonic, 0);45await setItem(VAULT_KEY, {6seed: await encrypt(password, mnemonic), // never stored in the clear7accounts: [{ address, kind: "hd", hdIndex: 0 }],8});910return { mnemonic }; // shown once, to you only11}
Neither the keystore nor the storage layer contains a single fetch call or URL. There is no code path from key material to the network.
Derivation: standard paths, so you can leave
Keys are derived at the paths every major wallet uses: m/44'/60'/0'/0/n for EVM chains and m/44'/501'/n'/0' for Solana. That matters more than it sounds: the same recovery phrase produces the same addresses in MetaMask and Phantom, which means your funds stay reachable without Impulse existing at all.
Solana uses a different curve, and this is where wallets quietly get it wrong. ed25519 requires SLIP-0010 rather than BIP-32, with every level hardened and no public derivation. Get it wrong and you produce a plausible-looking address that no other wallet agrees with. Impulse's implementation is checked against all four official SLIP-0010 test vectors, and the resulting addresses match what Phantom and Solflare show for the same phrase.
Encryption at rest
The phrase is sealed with a key derived from your password, using a fresh random salt and initialisation vector on every single write. Nothing is reused between encryptions, which is the property that makes AES-GCM safe to use at all.
1const PBKDF2_ITERS = 310_000; // OWASP floor for SHA-25623const salt = getRandomValues(new Uint8Array(16)); // fresh, every write4const iv = getRandomValues(new Uint8Array(12)); // never reused56const key = await deriveKey(password, salt); // PBKDF2 -> AES-2567const ct = await subtle.encrypt({ name: "AES-GCM", iv }, key, data);
The unlocked key lives in session memory only. It is cleared on lock, on browser close, and by an idle timeout enforced where signing happens rather than only in the interface.
The network boundary
A browser extension can only contact hosts declared in its manifest. The browser enforces this, not us. That list is the complete boundary of where anything could possibly go, and every entry is a public third-party API: chain RPCs, DexScreener, GeckoTerminal, GMGN, Jupiter, PumpPortal, Bubblemaps.
Not one of them is a server we operate. There is nowhere for your keys to go.
Verify it yourself
- Read the boundary. Open the extension's manifest and look at host_permissions. It is short, and it is the whole answer.
- Watch the network. Open devtools, create a wallet, and count the requests. There are none.
- Disconnect entirely. Turn off your network and create a wallet. It works, because the keys are made on your machine. A custodial wallet cannot do this.
- Take your keys elsewhere. Import the phrase into MetaMask or Phantom. Same addresses. Your funds do not depend on us.