Skip to content

Commit

Permalink
Added EIP-712 multi-dimensional array support (#687).
Browse files Browse the repository at this point in the history
  • Loading branch information
ricmoo committed Oct 12, 2020
1 parent 345a830 commit 5a4dd5a
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 91 deletions.
66 changes: 9 additions & 57 deletions packages/hash/src.ts/index.ts
Original file line number Diff line number Diff line change
@@ -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,
}
87 changes: 53 additions & 34 deletions packages/hash/src.ts/typed-data.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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);
Expand All @@ -35,6 +39,10 @@ const domainFieldTypes: Record<string, string> = {
salt: "bytes32"
};

const domainFieldNames: Array<string> = [
"name", "version", "chainId", "verifyingContract", "salt"
];

function getBaseEncoder(type: string): (value: any) => string {
// intXX and uintXX
{
Expand Down Expand Up @@ -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<string, any>) => {
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<any>) => {
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<string, any>) => {
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 {
Expand Down Expand Up @@ -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<TypedDataField> = [ ];
for (const name in domain) {
const type = domainFieldTypes[name];
Expand All @@ -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<string, Array<TypedDataField>>, value: Record<string, any>): string {
return keccak256(concat([
static encode(domain: TypedDataDomain, types: Record<string, Array<TypedDataField>>, value: Record<string, any>): string {
return hexConcat([
"0x1901",
TypedDataEncoder.hashTypedDataDomain(domain),
TypedDataEncoder.hashDomain(domain),
TypedDataEncoder.from(types).hash(value)
]));
]);
}

static hash(domain: TypedDataDomain, types: Record<string, Array<TypedDataField>>, value: Record<string, any>): string {
return keccak256(TypedDataEncoder.encode(domain, types, value));
}
}

0 comments on commit 5a4dd5a

Please sign in to comment.