From 5a4dd5a70377d3e86823d279d6ff466d03767644 Mon Sep 17 00:00:00 2001 From: Richard Moore Date: Mon, 12 Oct 2020 00:58:04 -0400 Subject: [PATCH] Added EIP-712 multi-dimensional array support (#687). --- packages/hash/src.ts/index.ts | 66 ++++------------------- packages/hash/src.ts/typed-data.ts | 87 ++++++++++++++++++------------ 2 files changed, 62 insertions(+), 91 deletions(-) diff --git a/packages/hash/src.ts/index.ts b/packages/hash/src.ts/index.ts index 464cde2e13..ec420e5746 100644 --- a/packages/hash/src.ts/index.ts +++ b/packages/hash/src.ts/index.ts @@ -1,67 +1,19 @@ "use strict"; -import { Bytes, concat, hexlify } from "@ethersproject/bytes"; -import { nameprep, toUtf8Bytes } from "@ethersproject/strings"; -import { keccak256 } from "@ethersproject/keccak256"; - -import { Logger } from "@ethersproject/logger"; -import { version } from "./_version"; -const logger = new Logger(version); +import { id } from "./id"; +import { isValidName, namehash } from "./namehash"; +import { hashMessage, messagePrefix } from "./message"; import { TypedDataEncoder as _TypedDataEncoder } from "./typed-data"; -import { id } from "./id"; - export { - _TypedDataEncoder, + id, - id -} - -/////////////////////////////// + namehash, + isValidName, -const Zeros = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); -const Partition = new RegExp("^((.*)\\.)?([^.]+)$"); - -export function isValidName(name: string): boolean { - try { - const comps = name.split("."); - for (let i = 0; i < comps.length; i++) { - if (nameprep(comps[i]).length === 0) { - throw new Error("empty") - } - } - return true; - } catch (error) { } - return false; -} + messagePrefix, + hashMessage, -export function namehash(name: string): string { - /* istanbul ignore if */ - if (typeof(name) !== "string") { - logger.throwArgumentError("invalid address - " + String(name), "name", name); - } - - let result: string | Uint8Array = Zeros; - while (name.length) { - const partition = name.match(Partition); - const label = toUtf8Bytes(nameprep(partition[3])); - result = keccak256(concat([result, keccak256(label)])); - - name = partition[2] || ""; - } - - return hexlify(result); -} - - -export const messagePrefix = "\x19Ethereum Signed Message:\n"; - -export function hashMessage(message: Bytes | string): string { - if (typeof(message) === "string") { message = toUtf8Bytes(message); } - return keccak256(concat([ - toUtf8Bytes(messagePrefix), - toUtf8Bytes(String(message.length)), - message - ])); + _TypedDataEncoder, } diff --git a/packages/hash/src.ts/typed-data.ts b/packages/hash/src.ts/typed-data.ts index f1cad53807..7863c5cd9d 100644 --- a/packages/hash/src.ts/typed-data.ts +++ b/packages/hash/src.ts/typed-data.ts @@ -1,7 +1,7 @@ import { TypedDataDomain, TypedDataField } from "@ethersproject/abstract-signer"; import { getAddress } from "@ethersproject/address"; import { BigNumber, BigNumberish } from "@ethersproject/bignumber"; -import { arrayify, BytesLike, concat, hexConcat, hexZeroPad } from "@ethersproject/bytes"; +import { arrayify, BytesLike, hexConcat, hexlify, hexZeroPad } from "@ethersproject/bytes"; import { keccak256 } from "@ethersproject/keccak256"; import { deepCopy, defineReadOnly } from "@ethersproject/properties"; @@ -21,7 +21,11 @@ const MaxUint256: BigNumber = BigNumber.from("0xffffffffffffffffffffffffffffffff function hexPadRight(value: BytesLike) { const bytes = arrayify(value); - return hexConcat([ bytes, padding.slice(bytes.length % 32) ]); + const padOffset = bytes.length % 32 + if (padOffset) { + return hexConcat([ bytes, padding.slice(padOffset) ]); + } + return hexlify(bytes); } const hexTrue = hexZeroPad(One.toHexString(), 32); @@ -35,6 +39,10 @@ const domainFieldTypes: Record = { salt: "bytes32" }; +const domainFieldNames: Array = [ + "name", "version", "chainId", "verifyingContract", "salt" +]; + function getBaseEncoder(type: string): (value: any) => string { // intXX and uintXX { @@ -217,47 +225,49 @@ export class TypedDataEncoder { } _getEncoder(type: string): (value: any) => string { - const match = type.match(/^([^\x5b]*)(\x5b(\d*)\x5d)?$/); - if (!match) { logger.throwArgumentError(`unknown type: ${ type }`, "type", type); } - - const baseType = match[1]; - - let baseEncoder = getBaseEncoder(baseType); - // A struct type - if (baseEncoder == null) { - const fields = this.types[baseType]; - if (!fields) { logger.throwArgumentError(`unknown type: ${ type }`, "type", type); } - - const encodedType = id(this._types[baseType]); - baseEncoder = (value: Record) => { - const values = fields.map((f) => { - const result = this.getEncoder(f.type)(value[f.name]); - if (this._types[f.type]) { return keccak256(result); } - return result; - }); - values.unshift(encodedType); - return hexConcat(values); - } + // Basic encoder type + { + const encoder = getBaseEncoder(type); + if (encoder) { return encoder; } } - // An array type - if (match[2]) { - const length = (match[3] ? parseInt(match[3]): -1); + // Array + const match = type.match(/^(.*)(\x5b(\d*)\x5d)$/); + if (match) { + const subtype = match[1]; + const subEncoder = this.getEncoder(subtype); + const length = parseInt(match[3]); return (value: Array) => { if (length >= 0 && value.length !== length) { logger.throwArgumentError("array length mismatch; expected length ${ arrayLength }", "value", value); } - let result = value.map(baseEncoder); - if (this._types[baseType]) { + let result = value.map(subEncoder); + if (this._types[subtype]) { result = result.map(keccak256); } + return keccak256(hexConcat(result)); }; } - return baseEncoder; + // Struct + const fields = this.types[type]; + if (fields) { + const encodedType = id(this._types[type]); + return (value: Record) => { + const values = fields.map((f) => { + const result = this.getEncoder(f.type)(value[f.name]); + if (this._types[f.type]) { return keccak256(result); } + return result; + }); + values.unshift(encodedType); + return hexConcat(values); + } + } + + return logger.throwArgumentError(`unknown type: ${ type }`, "type", type); } encodeType(name: string): string { @@ -296,7 +306,7 @@ export class TypedDataEncoder { return TypedDataEncoder.from(types).hashStruct(name, value); } - static hashTypedDataDomain(domain: TypedDataDomain): string { + static hashDomain(domain: TypedDataDomain): string { const domainFields: Array = [ ]; for (const name in domain) { const type = domainFieldTypes[name]; @@ -305,15 +315,24 @@ export class TypedDataEncoder { } domainFields.push({ name, type }); } + + domainFields.sort((a, b) => { + return domainFieldNames.indexOf(a.name) - domainFieldNames.indexOf(b.name); + }); + return TypedDataEncoder.hashStruct("EIP712Domain", { EIP712Domain: domainFields }, domain); } - static hashTypedData(domain: TypedDataDomain, types: Record>, value: Record): string { - return keccak256(concat([ + static encode(domain: TypedDataDomain, types: Record>, value: Record): string { + return hexConcat([ "0x1901", - TypedDataEncoder.hashTypedDataDomain(domain), + TypedDataEncoder.hashDomain(domain), TypedDataEncoder.from(types).hash(value) - ])); + ]); + } + + static hash(domain: TypedDataDomain, types: Record>, value: Record): string { + return keccak256(TypedDataEncoder.encode(domain, types, value)); } }