aboutsummaryrefslogtreecommitdiffstats
path: root/packages/order-utils/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/order-utils/src')
-rw-r--r--packages/order-utils/src/assert.ts27
-rw-r--r--packages/order-utils/src/constants.ts3
-rw-r--r--packages/order-utils/src/globals.d.ts6
-rw-r--r--packages/order-utils/src/index.ts6
-rw-r--r--packages/order-utils/src/monorepo_scripts/postpublish.ts8
-rw-r--r--packages/order-utils/src/monorepo_scripts/stage_docs.ts8
-rw-r--r--packages/order-utils/src/order_factory.ts49
-rw-r--r--packages/order-utils/src/order_hash.ts89
-rw-r--r--packages/order-utils/src/salt.ts18
-rw-r--r--packages/order-utils/src/signature_utils.ts119
-rw-r--r--packages/order-utils/src/types.ts3
11 files changed, 336 insertions, 0 deletions
diff --git a/packages/order-utils/src/assert.ts b/packages/order-utils/src/assert.ts
new file mode 100644
index 000000000..5ac402e7e
--- /dev/null
+++ b/packages/order-utils/src/assert.ts
@@ -0,0 +1,27 @@
+import { assert as sharedAssert } from '@0xproject/assert';
+// We need those two unused imports because they're actually used by sharedAssert which gets injected here
+// tslint:disable-next-line:no-unused-variable
+import { Schema } from '@0xproject/json-schemas';
+// tslint:disable-next-line:no-unused-variable
+import { ECSignature } from '@0xproject/types';
+import { BigNumber } from '@0xproject/utils';
+import { Web3Wrapper } from '@0xproject/web3-wrapper';
+import * as _ from 'lodash';
+
+import { isValidSignature } from './signature_utils';
+
+export const assert = {
+ ...sharedAssert,
+ async isSenderAddressAsync(
+ variableName: string,
+ senderAddressHex: string,
+ web3Wrapper: Web3Wrapper,
+ ): Promise<void> {
+ sharedAssert.isETHAddressHex(variableName, senderAddressHex);
+ const isSenderAddressAvailable = await web3Wrapper.isSenderAddressAvailableAsync(senderAddressHex);
+ sharedAssert.assert(
+ isSenderAddressAvailable,
+ `Specified ${variableName} ${senderAddressHex} isn't available through the supplied web3 provider`,
+ );
+ },
+};
diff --git a/packages/order-utils/src/constants.ts b/packages/order-utils/src/constants.ts
new file mode 100644
index 000000000..ec2fe744a
--- /dev/null
+++ b/packages/order-utils/src/constants.ts
@@ -0,0 +1,3 @@
+export const constants = {
+ NULL_ADDRESS: '0x0000000000000000000000000000000000000000',
+};
diff --git a/packages/order-utils/src/globals.d.ts b/packages/order-utils/src/globals.d.ts
new file mode 100644
index 000000000..94e63a32d
--- /dev/null
+++ b/packages/order-utils/src/globals.d.ts
@@ -0,0 +1,6 @@
+declare module '*.json' {
+ const json: any;
+ /* tslint:disable */
+ export default json;
+ /* tslint:enable */
+}
diff --git a/packages/order-utils/src/index.ts b/packages/order-utils/src/index.ts
new file mode 100644
index 000000000..2b565f713
--- /dev/null
+++ b/packages/order-utils/src/index.ts
@@ -0,0 +1,6 @@
+export { getOrderHashHex, isValidOrderHash } from './order_hash';
+export { isValidSignature, signOrderHashAsync } from './signature_utils';
+export { orderFactory } from './order_factory';
+export { generatePseudoRandomSalt } from './salt';
+export { constants } from './constants';
+export { OrderError } from './types';
diff --git a/packages/order-utils/src/monorepo_scripts/postpublish.ts b/packages/order-utils/src/monorepo_scripts/postpublish.ts
new file mode 100644
index 000000000..dcb99d0f7
--- /dev/null
+++ b/packages/order-utils/src/monorepo_scripts/postpublish.ts
@@ -0,0 +1,8 @@
+import { postpublishUtils } from '@0xproject/monorepo-scripts';
+
+import * as packageJSON from '../package.json';
+import * as tsConfigJSON from '../tsconfig.json';
+
+const cwd = `${__dirname}/..`;
+// tslint:disable-next-line:no-floating-promises
+postpublishUtils.runAsync(packageJSON, tsConfigJSON, cwd);
diff --git a/packages/order-utils/src/monorepo_scripts/stage_docs.ts b/packages/order-utils/src/monorepo_scripts/stage_docs.ts
new file mode 100644
index 000000000..e732ac8eb
--- /dev/null
+++ b/packages/order-utils/src/monorepo_scripts/stage_docs.ts
@@ -0,0 +1,8 @@
+import { postpublishUtils } from '@0xproject/monorepo-scripts';
+
+import * as packageJSON from '../package.json';
+import * as tsConfigJSON from '../tsconfig.json';
+
+const cwd = `${__dirname}/..`;
+// tslint:disable-next-line:no-floating-promises
+postpublishUtils.publishDocsToStagingAsync(packageJSON, tsConfigJSON, cwd);
diff --git a/packages/order-utils/src/order_factory.ts b/packages/order-utils/src/order_factory.ts
new file mode 100644
index 000000000..2759aac81
--- /dev/null
+++ b/packages/order-utils/src/order_factory.ts
@@ -0,0 +1,49 @@
+import { Provider, SignedOrder } from '@0xproject/types';
+import { BigNumber } from '@0xproject/utils';
+import * as _ from 'lodash';
+
+import { getOrderHashHex } from './order_hash';
+import { generatePseudoRandomSalt } from './salt';
+import { signOrderHashAsync } from './signature_utils';
+
+const SHOULD_ADD_PERSONAL_MESSAGE_PREFIX = false;
+
+export const orderFactory = {
+ async createSignedOrderAsync(
+ provider: Provider,
+ maker: string,
+ taker: string,
+ makerFee: BigNumber,
+ takerFee: BigNumber,
+ makerTokenAmount: BigNumber,
+ makerTokenAddress: string,
+ takerTokenAmount: BigNumber,
+ takerTokenAddress: string,
+ exchangeContractAddress: string,
+ feeRecipient: string,
+ expirationUnixTimestampSecIfExists?: BigNumber,
+ ): Promise<SignedOrder> {
+ const defaultExpirationUnixTimestampSec = new BigNumber(2524604400); // Close to infinite
+ const expirationUnixTimestampSec = _.isUndefined(expirationUnixTimestampSecIfExists)
+ ? defaultExpirationUnixTimestampSec
+ : expirationUnixTimestampSecIfExists;
+ const order = {
+ maker,
+ taker,
+ makerFee,
+ takerFee,
+ makerTokenAmount,
+ takerTokenAmount,
+ makerTokenAddress,
+ takerTokenAddress,
+ salt: generatePseudoRandomSalt(),
+ exchangeContractAddress,
+ feeRecipient,
+ expirationUnixTimestampSec,
+ };
+ const orderHash = getOrderHashHex(order);
+ const ecSignature = await signOrderHashAsync(provider, orderHash, maker, SHOULD_ADD_PERSONAL_MESSAGE_PREFIX);
+ const signedOrder: SignedOrder = _.assign(order, { ecSignature });
+ return signedOrder;
+ },
+};
diff --git a/packages/order-utils/src/order_hash.ts b/packages/order-utils/src/order_hash.ts
new file mode 100644
index 000000000..8da11c596
--- /dev/null
+++ b/packages/order-utils/src/order_hash.ts
@@ -0,0 +1,89 @@
+import { schemas, SchemaValidator } from '@0xproject/json-schemas';
+import { Order, SignedOrder, SolidityTypes } from '@0xproject/types';
+import { BigNumber } from '@0xproject/utils';
+import BN = require('bn.js');
+import * as ethABI from 'ethereumjs-abi';
+import * as ethUtil from 'ethereumjs-util';
+import * as _ from 'lodash';
+
+import { assert } from './assert';
+
+const INVALID_TAKER_FORMAT = 'instance.taker is not of a type(s) string';
+
+/**
+ * Converts BigNumber instance to BN
+ * The only reason we convert to BN is to remain compatible with `ethABI.soliditySHA3` that
+ * expects values of Solidity type `uint` to be passed as type `BN`.
+ * We do not use BN anywhere else in the codebase.
+ */
+function bigNumberToBN(value: BigNumber) {
+ return new BN(value.toString(), 10);
+}
+
+/**
+ * Computes the orderHash for a supplied order.
+ * @param order An object that conforms to the Order or SignedOrder interface definitions.
+ * @return The resulting orderHash from hashing the supplied order.
+ */
+export function getOrderHashHex(order: Order | SignedOrder): string {
+ try {
+ assert.doesConformToSchema('order', order, schemas.orderSchema);
+ } catch (error) {
+ if (_.includes(error.message, INVALID_TAKER_FORMAT)) {
+ const errMsg =
+ 'Order taker must be of type string. If you want anyone to be able to fill an order - pass ZeroEx.NULL_ADDRESS';
+ throw new Error(errMsg);
+ }
+ throw error;
+ }
+ const orderParts = [
+ { value: order.exchangeContractAddress, type: SolidityTypes.Address },
+ { value: order.maker, type: SolidityTypes.Address },
+ { value: order.taker, type: SolidityTypes.Address },
+ { value: order.makerTokenAddress, type: SolidityTypes.Address },
+ { value: order.takerTokenAddress, type: SolidityTypes.Address },
+ { value: order.feeRecipient, type: SolidityTypes.Address },
+ {
+ value: bigNumberToBN(order.makerTokenAmount),
+ type: SolidityTypes.Uint256,
+ },
+ {
+ value: bigNumberToBN(order.takerTokenAmount),
+ type: SolidityTypes.Uint256,
+ },
+ {
+ value: bigNumberToBN(order.makerFee),
+ type: SolidityTypes.Uint256,
+ },
+ {
+ value: bigNumberToBN(order.takerFee),
+ type: SolidityTypes.Uint256,
+ },
+ {
+ value: bigNumberToBN(order.expirationUnixTimestampSec),
+ type: SolidityTypes.Uint256,
+ },
+ { value: bigNumberToBN(order.salt), type: SolidityTypes.Uint256 },
+ ];
+ const types = _.map(orderParts, o => o.type);
+ const values = _.map(orderParts, o => o.value);
+ const hashBuff = ethABI.soliditySHA3(types, values);
+ const hashHex = ethUtil.bufferToHex(hashBuff);
+ return hashHex;
+}
+
+/**
+ * Checks if the supplied hex encoded order hash is valid.
+ * Note: Valid means it has the expected format, not that an order with the orderHash exists.
+ * Use this method when processing orderHashes submitted as user input.
+ * @param orderHash Hex encoded orderHash.
+ * @return Whether the supplied orderHash has the expected format.
+ */
+export function isValidOrderHash(orderHash: string): boolean {
+ // Since this method can be called to check if any arbitrary string conforms to an orderHash's
+ // format, we only assert that we were indeed passed a string.
+ assert.isString('orderHash', orderHash);
+ const schemaValidator = new SchemaValidator();
+ const isValid = schemaValidator.validate(orderHash, schemas.orderHashSchema).valid;
+ return isValid;
+}
diff --git a/packages/order-utils/src/salt.ts b/packages/order-utils/src/salt.ts
new file mode 100644
index 000000000..90a4197c0
--- /dev/null
+++ b/packages/order-utils/src/salt.ts
@@ -0,0 +1,18 @@
+import { BigNumber } from '@0xproject/utils';
+
+const MAX_DIGITS_IN_UNSIGNED_256_INT = 78;
+
+/**
+ * Generates a pseudo-random 256-bit salt.
+ * The salt can be included in a 0x order, ensuring that the order generates a unique orderHash
+ * and will not collide with other outstanding orders that are identical in all other parameters.
+ * @return A pseudo-random 256-bit number that can be used as a salt.
+ */
+export function generatePseudoRandomSalt(): BigNumber {
+ // BigNumber.random returns a pseudo-random number between 0 & 1 with a passed in number of decimal places.
+ // Source: https://mikemcl.github.io/bignumber.js/#random
+ const randomNumber = BigNumber.random(MAX_DIGITS_IN_UNSIGNED_256_INT);
+ const factor = new BigNumber(10).pow(MAX_DIGITS_IN_UNSIGNED_256_INT - 1);
+ const salt = randomNumber.times(factor).round();
+ return salt;
+}
diff --git a/packages/order-utils/src/signature_utils.ts b/packages/order-utils/src/signature_utils.ts
new file mode 100644
index 000000000..b511573a8
--- /dev/null
+++ b/packages/order-utils/src/signature_utils.ts
@@ -0,0 +1,119 @@
+import { schemas } from '@0xproject/json-schemas';
+import { ECSignature, Provider } from '@0xproject/types';
+import { Web3Wrapper } from '@0xproject/web3-wrapper';
+import * as ethUtil from 'ethereumjs-util';
+import * as _ from 'lodash';
+
+import { assert } from './assert';
+import { OrderError } from './types';
+
+/**
+ * Verifies that the elliptic curve signature `signature` was generated
+ * by signing `data` with the private key corresponding to the `signerAddress` address.
+ * @param data The hex encoded data signed by the supplied signature.
+ * @param signature An object containing the elliptic curve signature parameters.
+ * @param signerAddress The hex encoded address that signed the data, producing the supplied signature.
+ * @return Whether the signature is valid for the supplied signerAddress and data.
+ */
+export function isValidSignature(data: string, signature: ECSignature, signerAddress: string): boolean {
+ assert.isHexString('data', data);
+ assert.doesConformToSchema('signature', signature, schemas.ecSignatureSchema);
+ assert.isETHAddressHex('signerAddress', signerAddress);
+ const normalizedSignerAddress = signerAddress.toLowerCase();
+
+ const dataBuff = ethUtil.toBuffer(data);
+ const msgHashBuff = ethUtil.hashPersonalMessage(dataBuff);
+ try {
+ const pubKey = ethUtil.ecrecover(
+ msgHashBuff,
+ signature.v,
+ ethUtil.toBuffer(signature.r),
+ ethUtil.toBuffer(signature.s),
+ );
+ const retrievedAddress = ethUtil.bufferToHex(ethUtil.pubToAddress(pubKey));
+ return retrievedAddress === signerAddress;
+ } catch (err) {
+ return false;
+ }
+}
+/**
+ * Signs an orderHash and returns it's elliptic curve signature.
+ * This method currently supports TestRPC, Geth and Parity above and below V1.6.6
+ * @param orderHash Hex encoded orderHash to sign.
+ * @param signerAddress The hex encoded Ethereum address you wish to sign it with. This address
+ * must be available via the Provider supplied to 0x.js.
+ * @param shouldAddPersonalMessagePrefix Some signers add the personal message prefix `\x19Ethereum Signed Message`
+ * themselves (e.g Parity Signer, Ledger, TestRPC) and others expect it to already be done by the client
+ * (e.g Metamask). Depending on which signer this request is going to, decide on whether to add the prefix
+ * before sending the request.
+ * @return An object containing the Elliptic curve signature parameters generated by signing the orderHash.
+ */
+export async function signOrderHashAsync(
+ provider: Provider,
+ orderHash: string,
+ signerAddress: string,
+ shouldAddPersonalMessagePrefix: boolean,
+): Promise<ECSignature> {
+ assert.isHexString('orderHash', orderHash);
+ const web3Wrapper = new Web3Wrapper(provider);
+ await assert.isSenderAddressAsync('signerAddress', signerAddress, web3Wrapper);
+ const normalizedSignerAddress = signerAddress.toLowerCase();
+
+ let msgHashHex = orderHash;
+ if (shouldAddPersonalMessagePrefix) {
+ const orderHashBuff = ethUtil.toBuffer(orderHash);
+ const msgHashBuff = ethUtil.hashPersonalMessage(orderHashBuff);
+ msgHashHex = ethUtil.bufferToHex(msgHashBuff);
+ }
+
+ const signature = await web3Wrapper.signMessageAsync(normalizedSignerAddress, msgHashHex);
+
+ // HACK: There is no consensus on whether the signatureHex string should be formatted as
+ // v + r + s OR r + s + v, and different clients (even different versions of the same client)
+ // return the signature params in different orders. In order to support all client implementations,
+ // we parse the signature in both ways, and evaluate if either one is a valid signature.
+ const validVParamValues = [27, 28];
+ const ecSignatureVRS = parseSignatureHexAsVRS(signature);
+ if (_.includes(validVParamValues, ecSignatureVRS.v)) {
+ const isValidVRSSignature = isValidSignature(orderHash, ecSignatureVRS, normalizedSignerAddress);
+ if (isValidVRSSignature) {
+ return ecSignatureVRS;
+ }
+ }
+
+ const ecSignatureRSV = parseSignatureHexAsRSV(signature);
+ if (_.includes(validVParamValues, ecSignatureRSV.v)) {
+ const isValidRSVSignature = isValidSignature(orderHash, ecSignatureRSV, normalizedSignerAddress);
+ if (isValidRSVSignature) {
+ return ecSignatureRSV;
+ }
+ }
+
+ throw new Error(OrderError.InvalidSignature);
+}
+
+function parseSignatureHexAsVRS(signatureHex: string): ECSignature {
+ const signatureBuffer = ethUtil.toBuffer(signatureHex);
+ let v = signatureBuffer[0];
+ if (v < 27) {
+ v += 27;
+ }
+ const r = signatureBuffer.slice(1, 33);
+ const s = signatureBuffer.slice(33, 65);
+ const ecSignature: ECSignature = {
+ v,
+ r: ethUtil.bufferToHex(r),
+ s: ethUtil.bufferToHex(s),
+ };
+ return ecSignature;
+}
+
+function parseSignatureHexAsRSV(signatureHex: string): ECSignature {
+ const { v, r, s } = ethUtil.fromRpcSig(signatureHex);
+ const ecSignature: ECSignature = {
+ v,
+ r: ethUtil.bufferToHex(r),
+ s: ethUtil.bufferToHex(s),
+ };
+ return ecSignature;
+}
diff --git a/packages/order-utils/src/types.ts b/packages/order-utils/src/types.ts
new file mode 100644
index 000000000..f79d52359
--- /dev/null
+++ b/packages/order-utils/src/types.ts
@@ -0,0 +1,3 @@
+export enum OrderError {
+ InvalidSignature = 'INVALID_SIGNATURE',
+}