linera-io/linera-protocol · error · Error
MetaMask signer does not expose a public key; EVM signatures
Error message
MetaMask signer does not expose a public key; EVM signatures carry the address
What it means
MetaMask produces EVM secp256k1 signatures that embed the signer's address (AccountSignature::EvmSecp256k1), so a 'public key' API is meaningless for it and always throws. The Linera wasm bridge never calls getPublicKey on the Address20 path; hitting this error means application code is calling the signer directly, bypassing the bridge contract.
Source
Thrown at web/@linera/metamask/src/signer.ts:87
if (!signature) {
throw new Error("No signature returned");
}
return signature;
} catch (err: any) {
throw new Error(
`MetaMask signature request failed: ${err?.message || err}`,
);
}
}
async getPublicKey(_owner: string): Promise<string> {
// MetaMask signs only for `Address20` (EVM secp256k1) owners. The wasm bridge
// never calls `getPublicKey` on the Address20 path — EVM signatures carry the
// signer's address inline in `AccountSignature::EvmSecp256k1`. If we get here,
// a caller is reaching past the bridge contract.
throw new Error(
"MetaMask signer does not expose a public key; EVM signatures carry the address",
);
}
async containsKey(owner: string): Promise<boolean> {
const accounts = await this.provider.send("eth_requestAccounts", []);
return accounts.some(
(acc: string) => acc.toLowerCase() === owner.toLowerCase(),
);
}
/**
* Returns the currently connected MetaMask account address.
*/
async address(): Promise<string> {
const signer = await this.provider.getSigner();
let address = await signer.getAddress();
return address;View on GitHub (pinned to 6c226ddcb3)
Solutions
- Never call getPublicKey on the MetaMask signer; guard calls by owner format (Address20) or by signer capability.
- If you need the signer identity, recover it from a signature (ethers.verifyMessage) — the address is the identity.
- Keep a capability map (signer -> supports getPublicKey) when iterating heterogeneous signers.
Example fix
// before
for (const s of signers) await s.getPublicKey(owner); // throws on MetaMask signer
// after
for (const s of signers) {
if (s instanceof MetaMaskSigner) continue; // EVM sigs carry the address
await s.getPublicKey(owner);
} Defensive patterns
Strategy: type-guard
Validate before calling
const supportsPublicKey = (s: Signer): boolean => !(s instanceof MetaMaskSigner); if (supportsPublicKey(signer)) await signer.getPublicKey(owner);
Type guard
function exposesPublicKey(signer: Signer): boolean {
// Address20/EVM signers embed the address in signatures; they have no public-key API.
return !(signer instanceof MetaMaskSigner);
} Try / catch
try { await signer.getPublicKey(owner); }
catch (e) {
if (e instanceof Error && e.message.includes('does not expose a public key')) { /* recover address from the signature instead */ }
else throw e;
} Prevention
- Route public-key queries only to Ed25519 signers; use ethers.verifyMessage to recover an EVM signer's address.
- When iterating heterogeneous signers, gate calls per-capability instead of assuming the full interface.
When it happens
Trigger: Directly calling signer.getPublicKey(owner) on the MetaMask signer — e.g. generic code that enumerates all signers and calls getPublicKey on each, or reusing a public-key fetch routine written for the Ed25519 signer against every signer in a Composite.
Common situations: Refactoring owner/key inspection to treat all Signer implementations uniformly; tooling or debug panels that display public keys for every wallet; new callers assuming the SignerInterface contract is fully implemented by every signer.
Related errors
- No signature returned
- MetaMask signature request failed: ${err?.message || err}
- Returned AccountInfo should have code: Some(...) and so code
- Returned AccountInfo should have code: Some(...) and so code
- An Evm runtime is required to load user applications. Please
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/116d3670859d08d9.
Report an issue: GitHub.