{"record":{"id":"cb823492b0834a43","repo":"linera-io/linera-protocol","slug":"no-signature-returned","errorCode":null,"errorMessage":"No signature returned","messagePattern":"No signature returned","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"web/@linera/metamask/src/signer.ts","lineNumber":71,"sourceCode":"    const connected = accounts.find(\n      (acc) => acc.toLowerCase() === owner.toLowerCase(),\n    );\n    if (!connected) {\n      throw new Error(\n        `MetaMask is not connected with the requested owner: ${owner}`,\n      );\n    }\n\n    // Encode message as hex string\n    const msgHex = `0x${uint8ArrayToHex(value)}`;\n    try {\n      const signature = (await window.ethereum.request({\n        method: \"personal_sign\",\n        params: [msgHex, owner],\n      })) as string;\n\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    );","sourceCodeStart":53,"sourceCodeEnd":89,"githubUrl":"https://github.com/linera-io/linera-protocol/blob/6c226ddcb332ef55118dc8d0aafbd093d5420899/web/@linera/metamask/src/signer.ts#L53-L89","documentation":"sign() wraps personal_sign and treats a falsy return (undefined/null/empty string) as a protocol violation: the wallet acknowledged the request but produced no signature. Well-behaved wallets either return a 65-byte hex signature or reject, so this indicates a broken or non-conforming provider response rather than a user action.","triggerScenarios":"personal_sign resolves with undefined/null — stubbed/mocked window.ethereum in tests returning nothing, a wallet extension bug after update, or an exotic wallet that silently swallows the prompt.","commonSituations":"Test harnesses that mock eth_requestAccounts but forget to mock personal_sign; wallet extensions in a broken state after an update (restart fixes it); privacy/scanner extensions interfering with the provider's RPC responses.","solutions":["Retry the request once after a short delay; transient provider glitches often clear.","Ask the user to restart the browser/extension — a wedged content script returns empty responses.","In tests, make sure the personal_sign mock returns a hex string."],"exampleFix":"// before\nconst sig = await signer.sign(owner, bytes); // 'No signature returned'\n\n// after\nlet sig: string;\ntry {\n  sig = await signer.sign(owner, bytes);\n} catch (e) {\n  if (e instanceof Error && e.message === 'No signature returned') { /* retry once */ }\n  throw e;\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try { sig = await signer.sign(owner, bytes); }\ncatch (e) {\n  if (e instanceof Error && e.message === 'No signature returned') { sig = await signer.sign(owner, bytes); /* one retry */ }\n  else throw e;\n}","preventionTips":["In test harnesses, mock personal_sign to return a valid hex signature.","Ask users to restart the browser/extension when responses come back empty.","Log the raw provider response when this fires to identify non-conforming wallets."],"tags":["metamask","signature","personal-sign","rpc"],"backgroundTag":"wallet-signing-failed","analyzedSha":"6c226ddcb332ef55118dc8d0aafbd093d5420899","analyzedAt":"2026-08-22T22:49:09.787Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}