{"record":{"id":"116d3670859d08d9","repo":"linera-io/linera-protocol","slug":"metamask-signer-does-not-expose-a-public-key-evm","errorCode":null,"errorMessage":"MetaMask signer does not expose a public key; EVM signatures carry the address","messagePattern":"MetaMask signer does not expose a public key; EVM signatures carry the address","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"web/@linera/metamask/src/signer.ts","lineNumber":87,"sourceCode":"\n      if (!signature) {\n        throw new Error(\"No signature returned\");\n      }\n\n      return signature;\n    } catch (err: any) {\n      throw new Error(\n        `MetaMask signature request failed: ${err?.message || err}`,\n      );\n    }\n  }\n\n  async getPublicKey(_owner: string): Promise<string> {\n    // MetaMask signs only for `Address20` (EVM secp256k1) owners. The wasm bridge\n    // never calls `getPublicKey` on the Address20 path — EVM signatures carry the\n    // signer's address inline in `AccountSignature::EvmSecp256k1`. If we get here,\n    // a caller is reaching past the bridge contract.\n    throw new Error(\n      \"MetaMask signer does not expose a public key; EVM signatures carry the address\",\n    );\n  }\n\n  async containsKey(owner: string): Promise<boolean> {\n    const accounts = await this.provider.send(\"eth_requestAccounts\", []);\n    return accounts.some(\n      (acc: string) => acc.toLowerCase() === owner.toLowerCase(),\n    );\n  }\n\n  /**\n   * Returns the currently connected MetaMask account address.\n   */\n  async address(): Promise<string> {\n    const signer = await this.provider.getSigner();\n    let address = await signer.getAddress();\n    return address;","sourceCodeStart":69,"sourceCodeEnd":105,"githubUrl":"https://github.com/linera-io/linera-protocol/blob/6c226ddcb332ef55118dc8d0aafbd093d5420899/web/@linera/metamask/src/signer.ts#L69-L105","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nfor (const s of signers) await s.getPublicKey(owner); // throws on MetaMask signer\n\n// after\nfor (const s of signers) {\n  if (s instanceof MetaMaskSigner) continue; // EVM sigs carry the address\n  await s.getPublicKey(owner);\n}","handlingStrategy":"type-guard","validationCode":"const supportsPublicKey = (s: Signer): boolean => !(s instanceof MetaMaskSigner);\nif (supportsPublicKey(signer)) await signer.getPublicKey(owner);","typeGuard":"function exposesPublicKey(signer: Signer): boolean {\n  // Address20/EVM signers embed the address in signatures; they have no public-key API.\n  return !(signer instanceof MetaMaskSigner);\n}","tryCatchPattern":"try { await signer.getPublicKey(owner); }\ncatch (e) {\n  if (e instanceof Error && e.message.includes('does not expose a public key')) { /* recover address from the signature instead */ }\n  else throw e;\n}","preventionTips":["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."],"tags":["metamask","evm","public-key","signer-interface"],"backgroundTag":"unsupported-signer-operation","analyzedSha":"6c226ddcb332ef55118dc8d0aafbd093d5420899","analyzedAt":"2026-08-22T22:49:09.787Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}