From dd32eb4db2c732a4541d64de2c318c231460c559 Mon Sep 17 00:00:00 2001 From: Nexory Date: Tue, 4 Aug 2026 20:20:59 +0200 Subject: [PATCH] fix(eip712): infer root primaryType instead of first types key signTypedData fell back to the first non-EIP712Domain key when no primaryType was passed. EIP-712 defines the primary type as the struct not referenced as a field by any other struct, so a dependency declared before the root produced the wrong primaryType. inferPrimaryType now derives the root the way ethers.js does, and behaviour is unchanged when an explicit primaryType is passed or the root is declared first. Adds a regression test. --- src/__tests__/bridges.test.ts | 22 ++++++++++++++++++++++ src/bridges/ethers.ts | 22 ++++++++++++++++++++-- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/__tests__/bridges.test.ts b/src/__tests__/bridges.test.ts index dfd5d49..b09cde9 100644 --- a/src/__tests__/bridges.test.ts +++ b/src/__tests__/bridges.test.ts @@ -119,6 +119,28 @@ describe("EthersAdapterSigner", () => { }) }) + it("infers the root primaryType when omitted, not the first types key", async () => { + const adapter = createMockAdapter() + const signer = walletAdapterToEthersSigner(adapter, {}) + // `Person` (a dependency) is declared before `Mail` (the root). The primary + // type is the struct not referenced by any other, i.e. `Mail` - not the + // first key. + const types = { + Person: [{ name: "wallet", type: "address" }], + Mail: [ + { name: "from", type: "Person" }, + { name: "contents", type: "string" }, + ], + } + await signer.signTypedData({ name: "Test" }, types, { + from: { wallet: "0x1234567890abcdef1234567890abcdef12345678" }, + contents: "hi", + }) + expect(adapter.signTypedData).toHaveBeenCalledWith( + expect.objectContaining({ primaryType: "Mail" }), + ) + }) + it("connect returns new signer with different provider", () => { const adapter = createMockAdapter() const signer = walletAdapterToEthersSigner(adapter, { id: 1 }) diff --git a/src/bridges/ethers.ts b/src/bridges/ethers.ts index b186481..77c506c 100644 --- a/src/bridges/ethers.ts +++ b/src/bridges/ethers.ts @@ -108,8 +108,7 @@ export class EthersAdapterSigner { domain, types, message: value, - primaryType: - primaryType ?? Object.keys(types).find(t => t !== "EIP712Domain") ?? "", + primaryType: primaryType ?? inferPrimaryType(types), }) } @@ -117,3 +116,22 @@ export class EthersAdapterSigner { return new EthersAdapterSigner(this.adapter, provider) } } + +/** + * Infer the EIP-712 primary type the way ethers.js does: the struct that is not + * referenced as a field type by any other struct (the root of the type graph). + * The previous heuristic took the first key in `types`, which signs the wrong + * struct when the root is not declared first (e.g. dependencies listed above it). + */ +function inferPrimaryType(types: Record): string { + const named = Object.keys(types).filter(t => t !== "EIP712Domain") + const referenced = new Set() + for (const name of named) { + for (const field of types[name] ?? []) { + const base = String(field.type).replace(/(\[\d*\])+$/, "") + if (base in types) referenced.add(base) + } + } + const roots = named.filter(t => !referenced.has(t)) + return roots[0] ?? named[0] ?? "" +}