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] ?? "" +}