diff options
Diffstat (limited to 'packages/order-utils/src')
-rw-r--r-- | packages/order-utils/src/assert.ts | 27 | ||||
-rw-r--r-- | packages/order-utils/src/constants.ts | 3 | ||||
-rw-r--r-- | packages/order-utils/src/globals.d.ts | 6 | ||||
-rw-r--r-- | packages/order-utils/src/index.ts | 6 | ||||
-rw-r--r-- | packages/order-utils/src/monorepo_scripts/postpublish.ts | 8 | ||||
-rw-r--r-- | packages/order-utils/src/monorepo_scripts/stage_docs.ts | 8 | ||||
-rw-r--r-- | packages/order-utils/src/order_factory.ts | 49 | ||||
-rw-r--r-- | packages/order-utils/src/order_hash.ts | 89 | ||||
-rw-r--r-- | packages/order-utils/src/salt.ts | 18 | ||||
-rw-r--r-- | packages/order-utils/src/signature_utils.ts | 119 | ||||
-rw-r--r-- | packages/order-utils/src/types.ts | 3 |
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..b85432c84 --- /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 { constants } from './constants'; +export { generatePseudoRandomSalt } from './salt'; +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', +} |