diff --git a/src.ts/_tests/test-hash-typeddata.ts b/src.ts/_tests/test-hash-typeddata.ts index bf9a6ab501..57eec5b879 100644 --- a/src.ts/_tests/test-hash-typeddata.ts +++ b/src.ts/_tests/test-hash-typeddata.ts @@ -1,7 +1,6 @@ import assert from "assert"; import { loadTests } from "./utils.js"; import type { TestCaseTypedData } from "./types.js"; - import { TypedDataEncoder } from "../index.js"; @@ -18,3 +17,85 @@ describe("Tests Typed Data (EIP-712)", function() { }); } }); + +interface TestAlias { + name: string; + types: Record>; + typesAlias: Record>; + data: Record; + encoded: string; +} + +describe("Tests Typed Data (EIP-712) aliases", function() { + const tests: Array = [ + { + name: "uint", + types: { + foo: [ + { name: "a", type: "uint256" }, + { name: "b", type: "string" }, + ], + }, + typesAlias: { + foo: [ + { name: "a", type: "uint" }, + { name: "b", type: "string" }, + ], + }, + data: { + a: 35, + b: "hello" + }, + encoded: "0x859b6b4a5d436f85a809f6383b4b35a153aa6fe9c95946c366d9dfd634b89f4700000000000000000000000000000000000000000000000000000000000000231c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8" + }, + { + name: "int", + types: { + foo: [ + { name: "a", type: "int256" }, + { name: "b", type: "string" }, + ], + }, + typesAlias: { + foo: [ + { name: "a", type: "int" }, + { name: "b", type: "string" }, + ], + }, + data: { + a: 35, + b: "hello" + }, + encoded: "0xa272ada5f88085e4cb18acdb87bd057a8cbfec249fee53de0149409080947cf500000000000000000000000000000000000000000000000000000000000000231c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8" + }, + ]; + + for (const test of tests) { + it(`tests encoding typed-data: ${ test.name }`, function() { + const encoder = TypedDataEncoder.from(test.types); + assert.equal(encoder.primaryType, "foo", "primaryType"); + assert.equal(encoder.encodeData("foo", test.data), test.encoded, "encoded"); + }); + } + + it(`tests overriding an alias as a type`, function() { + const encoder = TypedDataEncoder.from({ + uint: [ + { name: "value", type: "uint256" } + ], + foo: [ + { name: "a", type: "uint" }, + { name: "b", type: "string" }, + ] + }); + assert.equal(encoder.primaryType, "foo", "primaryType"); + + const data = encoder.encodeData("foo", { + a: { value: 42 }, + b: "hello" + }); + + const encoded = "0x87a4bfff36f1a2ecde6468d6acd51ecc5ef8f3a15d8115a412c686d82d3fdbe4628fc3080b86a044fb60153bb7dc3f904e9ed1cebadf35c17099a060ba4df90b1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8"; + assert.equal(data, encoded, "encoded"); + }); +}); diff --git a/src.ts/hash/typed-data.ts b/src.ts/hash/typed-data.ts index 20d2bbf40f..e1e50ca92d 100644 --- a/src.ts/hash/typed-data.ts +++ b/src.ts/hash/typed-data.ts @@ -124,12 +124,12 @@ const domainChecks: Record any> = { function getBaseEncoder(type: string): null | ((value: any) => string) { // intXX and uintXX { - const match = type.match(/^(u?)int(\d*)$/); + const match = type.match(/^(u?)int(\d+)$/); if (match) { const signed = (match[1] === ""); - const width = parseInt(match[2] || "256"); - assertArgument(width % 8 === 0 && width !== 0 && width <= 256 && (match[2] == null || match[2] === String(width)), "invalid numeric width", "type", type); + const width = parseInt(match[2]); + assertArgument(width % 8 === 0 && width !== 0 && width <= 256 && match[2] === String(width), "invalid numeric width", "type", type); const boundsUpper = mask(BN_MAX_UINT256, signed ? (width - 1): width); const boundsLower = signed ? ((boundsUpper + BN_1) * BN__1): BN_0; @@ -220,8 +220,7 @@ export class TypedDataEncoder { * do not violate the [[link-eip-712]] structural constraints as * well as computes the [[primaryType]]. */ - constructor(types: Record>) { - this.#types = JSON.stringify(types); + constructor(_types: Record>) { this.#fullTypes = new Map(); this.#encoderCache = new Map(); @@ -234,11 +233,20 @@ export class TypedDataEncoder { // Link all subtypes within a given struct const subtypes: Map> = new Map(); - Object.keys(types).forEach((type) => { + const types: Record> = { }; + Object.keys(_types).forEach((type) => { + // Normalize int/uint unless they are a complex type themselves + types[type] = _types[type].map(({ name, type }) => { + if (type === "int" && !_types["int"]) { type = "int256"; } + if (type === "uint" && !_types["uint"]) { type = "uint256"; } + return { name, type }; + }); + links.set(type, new Set()); parents.set(type, [ ]); subtypes.set(type, new Set()); }); + this.#types = JSON.stringify(types); for (const name in types) { const uniqueNames: Set = new Set(); @@ -246,18 +254,18 @@ export class TypedDataEncoder { for (const field of types[name]) { // Check each field has a unique name - assertArgument(!uniqueNames.has(field.name), `duplicate variable name ${ JSON.stringify(field.name) } in ${ JSON.stringify(name) }`, "types", types); + assertArgument(!uniqueNames.has(field.name), `duplicate variable name ${ JSON.stringify(field.name) } in ${ JSON.stringify(name) }`, "types", _types); uniqueNames.add(field.name); // Get the base type (drop any array specifiers) const baseType = ((field.type.match(/^([^\x5b]*)(\x5b|$)/)))[1] || null; - assertArgument(baseType !== name, `circular type reference to ${ JSON.stringify(baseType) }`, "types", types); + assertArgument(baseType !== name, `circular type reference to ${ JSON.stringify(baseType) }`, "types", _types); // Is this a base encoding type? const encoder = getBaseEncoder(baseType); if (encoder) { continue; } - assertArgument(parents.has(baseType), `unknown type ${ JSON.stringify(baseType) }`, "types", types); + assertArgument(parents.has(baseType), `unknown type ${ JSON.stringify(baseType) }`, "types", _types); // Add linkage (parents.get(baseType) as Array).push(name); @@ -267,14 +275,14 @@ export class TypedDataEncoder { // Deduce the primary type const primaryTypes = Array.from(parents.keys()).filter((n) => ((parents.get(n) as Array).length === 0)); - assertArgument(primaryTypes.length !== 0, "missing primary type", "types", types); - assertArgument(primaryTypes.length === 1, `ambiguous primary types or unused types: ${ primaryTypes.map((t) => (JSON.stringify(t))).join(", ") }`, "types", types); + assertArgument(primaryTypes.length !== 0, "missing primary type", "types", _types); + assertArgument(primaryTypes.length === 1, `ambiguous primary types or unused types: ${ primaryTypes.map((t) => (JSON.stringify(t))).join(", ") }`, "types", _types); defineProperties(this, { primaryType: primaryTypes[0] }); // Check for circular type references function checkCircular(type: string, found: Set) { - assertArgument(!found.has(type), `circular type reference to ${ JSON.stringify(type) }`, "types", types); + assertArgument(!found.has(type), `circular type reference to ${ JSON.stringify(type) }`, "types", _types); found.add(type);