Skip to content

Commit

Permalink
Merge pull request #2 from Loopring/EIP712
Browse files Browse the repository at this point in the history
Eip712
  • Loading branch information
Brechtpd authored Nov 13, 2018
2 parents 831e8ad + 19fa956 commit c6e4cc6
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 67 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "protocol2-js",
"version": "0.2.16",
"version": "0.2.18",
"description": "loopring protocol simulator core lib",
"main": "index.ts",
"repository": {
Expand Down
106 changes: 106 additions & 0 deletions src/eip712.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import BN = require("bn.js");
import ABI = require("ethereumjs-abi");
import ethUtil = require("ethereumjs-util");

const TYPED_MESSAGE_SCHEMA = {
type: "object",
properties: {
types: {
type: "object",
additionalProperties: {
type: "array",
items: {
type: "object",
properties: {
name: {type: "string"},
type: {type: "string"},
},
required: ["name", "type"],
},
},
},
primaryType: {type: "string"},
domain: {type: "object"},
message: {type: "object"},
},
required: ["types", "primaryType", "domain", "message"],
};

export function getEIP712Message(typedData: any) {
const sanitizedData = sanitizeData(typedData);
const parts = [Buffer.from("1901", "hex")];
parts.push(hashStruct("EIP712Domain", sanitizedData.domain, sanitizedData.types));
parts.push(hashStruct(sanitizedData.primaryType, sanitizedData.message, sanitizedData.types));
return ethUtil.sha3(Buffer.concat(parts));
}

function sanitizeData(data: any) {
const sanitizedData: any = {};
for (const key in TYPED_MESSAGE_SCHEMA.properties) {
if (key && data[key]) {
sanitizedData[key] = data[key];
}
}
return sanitizedData;
}

function hashStruct(primaryType: any, data: any, types: any) {
const encodedData = encodeData(primaryType, data, types);
return ethUtil.sha3(encodeData(primaryType, data, types));
}

function encodeData(primaryType: any, data: any, types: any) {
const encodedTypes = ["bytes32"];
const encodedValues = [hashType(primaryType, types)];
for (const field of types[primaryType]) {
let value = data[field.name];
if (value !== undefined) {
if (field.type === "string" || field.type === "bytes") {
encodedTypes.push("bytes32");
value = ethUtil.sha3(value);
encodedValues.push(value);
} else if (types[field.type] !== undefined) {
encodedTypes.push("bytes32");
value = ethUtil.sha3(encodeData(field.type, value, types));
encodedValues.push(value);
} else if (field.type.lastIndexOf("]") === field.type.length - 1) {
throw new Error("Arrays currently unimplemented in encodeData");
} else {
encodedTypes.push(field.type);
encodedValues.push(value);
}
}
}
return ABI.rawEncode(encodedTypes, encodedValues);
}

function hashType(primaryType: any, types: any) {
return ethUtil.sha3(encodeType(primaryType, types));
}

function encodeType(primaryType: any, types: any) {
let result = "";
let deps = findTypeDependencies(primaryType, types).filter((dep: any) => dep !== primaryType);
deps = [primaryType].concat(deps.sort());
for (const type of deps) {
const children = types[type];
if (!children) {
throw new Error("No type definition specified: ${type}");
}
result += `${type}(${types[type].map((o: any) => `${o.type} ${o.name}`).join(",")})`;
}
return result;
}

function findTypeDependencies(primaryType: any, types: any, results: any = []) {
if (results.includes(primaryType) || types[primaryType] === undefined) { return results; }
results.push(primaryType);
for (const field of types[primaryType]) {
for (const dep of findTypeDependencies(field.type, types, results)) {
if (!results.includes(dep)) {
results.push(dep);
}
}
}
return results;
}
49 changes: 24 additions & 25 deletions src/multihash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ export class MultiHashUtil {

public async signOrderAsync(order: OrderInfo) {
const signer = order.broker ? order.broker : order.owner;
return await this.signAsync(order.signAlgorithm, order.hash, signer);
return await this.signAsync(order.signAlgorithm, order.hash, signer, order.signerPrivateKey);
}

public async signAsync(algorithm: SignAlgorithm, hash: Buffer, address: string) {
public async signAsync(algorithm: SignAlgorithm, hash: Buffer, address: string, privateKey?: string) {
// Default to standard Ethereum signing
algorithm = Object.is(algorithm, undefined) ? SignAlgorithm.Ethereum : algorithm;

Expand All @@ -40,7 +40,7 @@ export class MultiHashUtil {
await this.signEthereumAsync(sig, hash, address);
return sig.getData();
case SignAlgorithm.EIP712:
await this.signEIP712Async(sig, hash, address);
await this.signEIP712Async(sig, hash, privateKey);
return sig.getData();
case SignAlgorithm.None:
return null;
Expand Down Expand Up @@ -72,6 +72,21 @@ export class MultiHashUtil {
} catch {
return false;
}
} else if (algorithm === SignAlgorithm.EIP712) {
assert.notEqual(signer, "0x0", "invalid signer address");
assert.equal(size, 65, "bad EIP712 multihash size");

const v = bitstream.extractUint8(2);
const r = bitstream.extractBytes32(3);
const s = bitstream.extractBytes32(3 + 32);

try {
const pub = ethUtil.ecrecover(hash, v, r, s);
const recoveredAddress = "0x" + ethUtil.pubToAddress(pub).toString("hex");
return signer === recoveredAddress;
} catch {
return false;
}
} else {
return false;
}
Expand All @@ -87,29 +102,13 @@ export class MultiHashUtil {
sig.addHex(ethUtil.bufferToHex(s));
}

// TODO: Actually implement this correctly, the standard is not widely supported yet
private async signEIP712Async(sig: Bitstream, hash: Buffer, address: string) {
throw Error("EIP712 signing currently not implemented.");

/*const orderHash = this.getOrderHash(order);
const msgParams = [
{type: "string", name: "Owner", value: order.owner},
];
const signature = await web3.eth.signTypedData(msgParams, order.owner);
const { v, r, s } = ethUtil.fromRpcSig(signature);
// await web3.currentProvider.sendAsync({
// method: "eth_signTypedData",
// params: [msgParams, order.owner],
// from: order.owner,
// }, (err?: Error, result?: Web3.JSONRPCResponsePayload) => { logDebug("Hashing: " + result.result); });
private async signEIP712Async(sig: Bitstream, hash: Buffer, privateKey: string) {
const signature = ethUtil.ecsign(hash, new Buffer(privateKey, "hex"));
// console.log(privateKey);
sig.addNumber(1 + 32 + 32, 1);
sig.addNumber(v, 1);
sig.addHex(ethUtil.bufferToHex(r));
sig.addHex(ethUtil.bufferToHex(s));*/
sig.addNumber(signature.v, 1);
sig.addHex(ethUtil.bufferToHex(signature.r));
sig.addHex(ethUtil.bufferToHex(signature.s));
}

}
115 changes: 74 additions & 41 deletions src/order.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import BN = require("bn.js");
import ABI = require("ethereumjs-abi");
import { Bitstream } from "./bitstream";
import { Context } from "./context";
import { getEIP712Message } from "./eip712";
import { ensure } from "./ensure";
import { MultiHashUtil } from "./multihash";
import { OrderInfo, Spendable } from "./types";
Expand Down Expand Up @@ -83,48 +84,80 @@ export class OrderUtil {
}
}

public toTypedData(order: OrderInfo) {
const typedData = {
types: {
EIP712Domain: [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
],
Order: [
{ name: "amountS", type: "uint" },
{ name: "amountB", type: "uint" },
{ name: "feeAmount", type: "uint" },
{ name: "validSince", type: "uint" },
{ name: "validUntil", type: "uint" },
{ name: "owner", type: "address" },
{ name: "tokenS", type: "address" },
{ name: "tokenB", type: "address" },
{ name: "dualAuthAddr", type: "address" },
{ name: "broker", type: "address" },
{ name: "orderInterceptor", type: "address" },
{ name: "wallet", type: "address" },
{ name: "tokenRecipient", type: "address" },
{ name: "feeToken", type: "address" },
{ name: "walletSplitPercentage", type: "uint16" },
{ name: "tokenSFeePercentage", type: "uint16" },
{ name: "tokenBFeePercentage", type: "uint16" },
{ name: "allOrNone", type: "bool" },
],
},
primaryType: "Order",
domain: {
name: "Loopring Protocol",
version: "2",
},
message: {
amountS: this.toBN(order.amountS),
amountB: this.toBN(order.amountB),
feeAmount: this.toBN(order.feeAmount),
validSince: order.validSince ? this.toBN(order.validSince) : this.toBN(0),
validUntil: order.validUntil ? this.toBN(order.validUntil) : this.toBN(0),
owner: order.owner,
tokenS: order.tokenS,
tokenB: order.tokenB,
dualAuthAddr: order.dualAuthAddr ? order.dualAuthAddr : "0x0",
broker: order.broker ? order.broker : "0x0",
orderInterceptor: order.orderInterceptor ? order.orderInterceptor : "0x0",
wallet: order.walletAddr ? order.walletAddr : "0x0",
tokenRecipient: order.tokenRecipient,
feeToken: order.feeToken,
walletSplitPercentage: order.walletSplitPercentage,
tokenSFeePercentage: order.tokenSFeePercentage,
tokenBFeePercentage: order.tokenBFeePercentage,
allOrNone: order.allOrNone,
},
};
return typedData;
}

public toTypedDataJSON(order: OrderInfo) {
// BN outputs hex numbers in toJSON, but signTypedData expects decimal numbers
const replacer = (key: any, value: any) => {
if (key === "amountS" || key === "amountB" || key === "feeAmount" ||
key === "validSince" || key === "validUntil") {
return "" + parseInt(value, 16);
}
return value;
};
const typedData = this.toTypedData(order);
const json = JSON.stringify(typedData, replacer);
return json;
}

public getOrderHash(order: OrderInfo) {
const args = [
this.toBN(order.amountS),
this.toBN(order.amountB),
this.toBN(order.feeAmount),
order.validSince ? this.toBN(order.validSince) : this.toBN(0),
order.validUntil ? this.toBN(order.validUntil) : this.toBN(0),
order.owner,
order.tokenS,
order.tokenB,
order.dualAuthAddr ? order.dualAuthAddr : "0x0",
order.broker ? order.broker : "0x0",
order.orderInterceptor ? order.orderInterceptor : "0x0",
order.walletAddr ? order.walletAddr : "0x0",
order.tokenRecipient,
order.feeToken,
order.walletSplitPercentage,
this.toBN(order.tokenSFeePercentage),
this.toBN(order.tokenBFeePercentage),
order.allOrNone,
];
const argTypes = [
"uint256",
"uint256",
"uint256",
"uint256",
"uint256",
"address",
"address",
"address",
"address",
"address",
"address",
"address",
"address",
"address",
"uint16",
"uint16",
"uint16",
"bool",
];
const orderHash = ABI.soliditySHA3(argTypes, args);
const typedData = this.toTypedData(order);
const orderHash = getEIP712Message(typedData);
return orderHash;
}

Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export interface OrderInfo {
balanceFee?: number;
balanceB?: number;
onChain?: boolean;
signerPrivateKey?: string;

[key: string]: any;
}
Expand Down

0 comments on commit c6e4cc6

Please sign in to comment.