aboutsummaryrefslogtreecommitdiffstats
path: root/packages/order-utils
diff options
context:
space:
mode:
authorFabio Berger <me@fabioberger.com>2018-08-15 05:21:47 +0800
committerFabio Berger <me@fabioberger.com>2018-08-15 05:21:47 +0800
commit2f2582a0da3095d61a99ef09744dc0995677558e (patch)
tree54989565919038c42d495dafb7426d4148605e84 /packages/order-utils
parent8169155a6547fb0283cd0f5362aad3c0b173b00b (diff)
parentfadd292ecf367e42154856509d0ea0c20b23f2f1 (diff)
downloaddexon-0x-contracts-2f2582a0da3095d61a99ef09744dc0995677558e.tar
dexon-0x-contracts-2f2582a0da3095d61a99ef09744dc0995677558e.tar.gz
dexon-0x-contracts-2f2582a0da3095d61a99ef09744dc0995677558e.tar.bz2
dexon-0x-contracts-2f2582a0da3095d61a99ef09744dc0995677558e.tar.lz
dexon-0x-contracts-2f2582a0da3095d61a99ef09744dc0995677558e.tar.xz
dexon-0x-contracts-2f2582a0da3095d61a99ef09744dc0995677558e.tar.zst
dexon-0x-contracts-2f2582a0da3095d61a99ef09744dc0995677558e.zip
Merge development
Diffstat (limited to 'packages/order-utils')
-rw-r--r--packages/order-utils/CHANGELOG.json23
-rw-r--r--packages/order-utils/CHANGELOG.md11
-rw-r--r--packages/order-utils/package.json30
-rw-r--r--packages/order-utils/src/constants.ts3
-rw-r--r--packages/order-utils/src/index.ts4
-rw-r--r--packages/order-utils/src/market_utils.ts133
-rw-r--r--packages/order-utils/src/order_factory.ts97
-rw-r--r--packages/order-utils/src/signature_utils.ts128
-rw-r--r--packages/order-utils/src/types.ts34
-rw-r--r--packages/order-utils/test/market_utils_test.ts280
-rw-r--r--packages/order-utils/test/signature_utils_test.ts142
-rw-r--r--packages/order-utils/test/utils/test_order_factory.ts32
12 files changed, 755 insertions, 162 deletions
diff --git a/packages/order-utils/CHANGELOG.json b/packages/order-utils/CHANGELOG.json
index a399f5ea1..a81eb077d 100644
--- a/packages/order-utils/CHANGELOG.json
+++ b/packages/order-utils/CHANGELOG.json
@@ -1,5 +1,28 @@
[
{
+ "version": "1.0.1-rc.3",
+ "changes": [
+ {
+ "pr": 914,
+ "note":
+ "Update ecSignOrderHashAsync to return signature string with signature type byte. Removes messagePrefixOpts."
+ },
+ {
+ "note":
+ "Added a synchronous `createOrder` method in `orderFactory`, updated public interfaces to support some optional parameters",
+ "pr": 936
+ },
+ {
+ "note": "Added marketUtils",
+ "pr": 937
+ },
+ {
+ "note": "Dependencies updated"
+ }
+ ],
+ "timestamp": 1534210131
+ },
+ {
"version": "1.0.1-rc.2",
"changes": [
{
diff --git a/packages/order-utils/CHANGELOG.md b/packages/order-utils/CHANGELOG.md
index 15b7e5ca6..0df2a6a75 100644
--- a/packages/order-utils/CHANGELOG.md
+++ b/packages/order-utils/CHANGELOG.md
@@ -5,6 +5,13 @@ Edit the package's CHANGELOG.json file only.
CHANGELOG
+## v1.0.1-rc.3 - _August 13, 2018_
+
+ * Update ecSignOrderHashAsync to return signature string with signature type byte. Removes messagePrefixOpts. (#914)
+ * Added a synchronous `createOrder` method in `orderFactory`, updated public interfaces to support some optional parameters (#936)
+ * Added marketUtils (#937)
+ * Dependencies updated
+
## v1.0.1-rc.2 - _July 26, 2018_
* Dependencies updated
@@ -21,7 +28,7 @@ CHANGELOG
* Upgrade ethereumjs-abi dep including a fix so that addresses starting with 0 are properly decoded by `decodeERC20AssetData`
-## v1.0.0-rc.1 - _July 20, 2018_
+## v1.0.0-rc.1 - _July 19, 2018_
* Refactor to work with V2 of 0x protocol (#636)
* Export parseECSignature method (#684)
@@ -47,7 +54,7 @@ CHANGELOG
* Add orderStateUtils, a module for computing order state needed to decide if an order is still valid
-## v0.0.4 - _May 5, 2018_
+## v0.0.4 - _May 4, 2018_
* Dependencies updated
diff --git a/packages/order-utils/package.json b/packages/order-utils/package.json
index c49abfdae..bcfb0611f 100644
--- a/packages/order-utils/package.json
+++ b/packages/order-utils/package.json
@@ -1,6 +1,6 @@
{
"name": "@0xproject/order-utils",
- "version": "1.0.1-rc.2",
+ "version": "1.0.1-rc.3",
"engines": {
"node": ">=6.12"
},
@@ -44,8 +44,8 @@
},
"homepage": "https://github.com/0xProject/0x-monorepo/packages/order-utils/README.md",
"devDependencies": {
- "@0xproject/dev-utils": "^1.0.3",
- "@0xproject/tslint-config": "^1.0.4",
+ "@0xproject/dev-utils": "^1.0.4",
+ "@0xproject/tslint-config": "^1.0.5",
"@types/bn.js": "^4.11.0",
"@types/lodash": "4.14.104",
"chai": "^4.0.1",
@@ -54,30 +54,30 @@
"copyfiles": "^1.2.0",
"dirty-chai": "^2.0.1",
"make-promises-safe": "^1.1.0",
- "mocha": "^4.0.1",
+ "mocha": "^4.1.0",
"npm-run-all": "^4.1.2",
"shx": "^0.2.2",
"sinon": "^4.0.0",
"tslint": "5.11.0",
"typedoc": "0xProject/typedoc",
- "typescript": "2.7.1"
+ "typescript": "2.9.2"
},
"dependencies": {
- "@0xproject/assert": "^1.0.4",
- "@0xproject/base-contract": "^1.0.4",
- "@0xproject/json-schemas": "^1.0.1-rc.3",
- "@0xproject/sol-compiler": "^1.0.4",
- "@0xproject/types": "^1.0.1-rc.3",
- "@0xproject/typescript-typings": "^1.0.3",
- "@0xproject/utils": "^1.0.4",
- "@0xproject/web3-wrapper": "^1.1.2",
+ "@0xproject/assert": "^1.0.5",
+ "@0xproject/base-contract": "^2.0.0-rc.1",
+ "@0xproject/json-schemas": "^1.0.1-rc.4",
+ "@0xproject/sol-compiler": "^1.0.5",
+ "@0xproject/types": "^1.0.1-rc.4",
+ "@0xproject/typescript-typings": "^1.0.4",
+ "@0xproject/utils": "^1.0.5",
+ "@0xproject/web3-wrapper": "^1.2.0",
"@types/node": "^8.0.53",
"bn.js": "^4.11.8",
- "ethereum-types": "^1.0.3",
+ "ethereum-types": "^1.0.4",
"ethereumjs-abi": "0.6.5",
"ethereumjs-util": "^5.1.1",
"ethers": "3.0.22",
- "lodash": "^4.17.4"
+ "lodash": "^4.17.5"
},
"publishConfig": {
"access": "public"
diff --git a/packages/order-utils/src/constants.ts b/packages/order-utils/src/constants.ts
index bb7482184..c23578c20 100644
--- a/packages/order-utils/src/constants.ts
+++ b/packages/order-utils/src/constants.ts
@@ -2,6 +2,7 @@ import { BigNumber } from '@0xproject/utils';
export const constants = {
NULL_ADDRESS: '0x0000000000000000000000000000000000000000',
+ NULL_BYTES: '0x',
// tslint:disable-next-line:custom-no-magic-numbers
UNLIMITED_ALLOWANCE_IN_BASE_UNITS: new BigNumber(2).pow(256).minus(1),
TESTRPC_NETWORK_ID: 50,
@@ -10,4 +11,6 @@ export const constants = {
ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH: 53,
SELECTOR_LENGTH: 4,
BASE_16: 16,
+ INFINITE_TIMESTAMP_SEC: new BigNumber(2524604400), // Close to infinite
+ ZERO_AMOUNT: new BigNumber(0),
};
diff --git a/packages/order-utils/src/index.ts b/packages/order-utils/src/index.ts
index f3b00a583..638126af6 100644
--- a/packages/order-utils/src/index.ts
+++ b/packages/order-utils/src/index.ts
@@ -3,6 +3,7 @@ export { signatureUtils } from './signature_utils';
export { generatePseudoRandomSalt } from './salt';
export { assetDataUtils } from './asset_data_utils';
export { eip712Utils } from './eip712_utils';
+export { marketUtils } from './market_utils';
export { OrderStateUtils } from './order_state_utils';
export { AbstractBalanceAndProxyAllowanceFetcher } from './abstract/abstract_balance_and_proxy_allowance_fetcher';
@@ -25,9 +26,8 @@ export {
AssetProxyId,
} from '@0xproject/types';
export {
+ CreateOrderOpts,
OrderError,
- MessagePrefixType,
- MessagePrefixOpts,
EIP712Parameter,
EIP712Schema,
EIP712Types,
diff --git a/packages/order-utils/src/market_utils.ts b/packages/order-utils/src/market_utils.ts
new file mode 100644
index 000000000..681059ddf
--- /dev/null
+++ b/packages/order-utils/src/market_utils.ts
@@ -0,0 +1,133 @@
+import { schemas } from '@0xproject/json-schemas';
+import { SignedOrder } from '@0xproject/types';
+import { BigNumber } from '@0xproject/utils';
+import * as _ from 'lodash';
+
+import { assert } from './assert';
+import { constants } from './constants';
+
+export const marketUtils = {
+ /**
+ * Takes an array of orders and returns a subset of those orders that has enough makerAssetAmount (taking into account on-chain balances,
+ * allowances, and partial fills) in order to fill the input makerAssetFillAmount plus slippageBufferAmount. Iterates from first order to last.
+ * Sort the input by ascending rate in order to get the subset of orders that will cost the least ETH.
+ * @param signedOrders An array of objects that conform to the SignedOrder interface. All orders should specify the same makerAsset.
+ * All orders should specify WETH as the takerAsset.
+ * @param remainingFillableMakerAssetAmounts An array of BigNumbers corresponding to the signedOrders parameter.
+ * You can use OrderStateUtils @0xproject/order-utils to perform blockchain lookups
+ * for these values.
+ * @param makerAssetFillAmount The amount of makerAsset desired to be filled.
+ * @param slippageBufferAmount An additional amount of makerAsset to be covered by the result in case of trade collisions or partial fills.
+ * @return Resulting orders and remaining fill amount that could not be covered by the input.
+ */
+ findOrdersThatCoverMakerAssetFillAmount(
+ signedOrders: SignedOrder[],
+ remainingFillableMakerAssetAmounts: BigNumber[],
+ makerAssetFillAmount: BigNumber,
+ slippageBufferAmount: BigNumber = constants.ZERO_AMOUNT,
+ ): { resultOrders: SignedOrder[]; remainingFillAmount: BigNumber } {
+ assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema);
+ _.forEach(remainingFillableMakerAssetAmounts, (amount, index) =>
+ assert.isValidBaseUnitAmount(`remainingFillableMakerAssetAmount[${index}]`, amount),
+ );
+ assert.isValidBaseUnitAmount('makerAssetFillAmount', makerAssetFillAmount);
+ assert.isValidBaseUnitAmount('slippageBufferAmount', slippageBufferAmount);
+ assert.assert(
+ signedOrders.length === remainingFillableMakerAssetAmounts.length,
+ 'Expected signedOrders.length to equal remainingFillableMakerAssetAmounts.length',
+ );
+ // calculate total amount of makerAsset needed to be filled
+ const totalFillAmount = makerAssetFillAmount.plus(slippageBufferAmount);
+ // iterate through the signedOrders input from left to right until we have enough makerAsset to fill totalFillAmount
+ const result = _.reduce(
+ signedOrders,
+ ({ resultOrders, remainingFillAmount }, order, index) => {
+ if (remainingFillAmount.lessThanOrEqualTo(constants.ZERO_AMOUNT)) {
+ return { resultOrders, remainingFillAmount: constants.ZERO_AMOUNT };
+ } else {
+ const makerAssetAmountAvailable = remainingFillableMakerAssetAmounts[index];
+ // if there is no makerAssetAmountAvailable do not append order to resultOrders
+ // if we have exceeded the total amount we want to fill set remainingFillAmount to 0
+ return {
+ resultOrders: makerAssetAmountAvailable.gt(constants.ZERO_AMOUNT)
+ ? _.concat(resultOrders, order)
+ : resultOrders,
+ remainingFillAmount: BigNumber.max(
+ constants.ZERO_AMOUNT,
+ remainingFillAmount.minus(makerAssetAmountAvailable),
+ ),
+ };
+ }
+ },
+ { resultOrders: [] as SignedOrder[], remainingFillAmount: totalFillAmount },
+ );
+ return result;
+ },
+ /**
+ * Takes an array of orders and an array of feeOrders. Returns a subset of the feeOrders that has enough ZRX (taking into account
+ * on-chain balances, allowances, and partial fills) in order to fill the takerFees required by signedOrders plus a
+ * slippageBufferAmount. Iterates from first feeOrder to last. Sort the feeOrders by ascending rate in order to get the subset of
+ * feeOrders that will cost the least ETH.
+ * @param signedOrders An array of objects that conform to the SignedOrder interface. All orders should specify ZRX as
+ * the makerAsset and WETH as the takerAsset.
+ * @param remainingFillableMakerAssetAmounts An array of BigNumbers corresponding to the signedOrders parameter.
+ * You can use OrderStateUtils @0xproject/order-utils to perform blockchain lookups
+ * for these values.
+ * @param signedFeeOrders An array of objects that conform to the SignedOrder interface. All orders should specify ZRX as
+ * the makerAsset and WETH as the takerAsset.
+ * @param remainingFillableFeeAmounts An array of BigNumbers corresponding to the signedFeeOrders parameter.
+ * You can use OrderStateUtils @0xproject/order-utils to perform blockchain lookups
+ * for these values.
+ * @param slippageBufferAmount An additional amount of fee to be covered by the result in case of trade collisions or partial fills.
+ * @return Resulting orders and remaining fee amount that could not be covered by the input.
+ */
+ findFeeOrdersThatCoverFeesForTargetOrders(
+ signedOrders: SignedOrder[],
+ remainingFillableMakerAssetAmounts: BigNumber[],
+ signedFeeOrders: SignedOrder[],
+ remainingFillableFeeAmounts: BigNumber[],
+ slippageBufferAmount: BigNumber = constants.ZERO_AMOUNT,
+ ): { resultOrders: SignedOrder[]; remainingFeeAmount: BigNumber } {
+ assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema);
+ _.forEach(remainingFillableMakerAssetAmounts, (amount, index) =>
+ assert.isValidBaseUnitAmount(`remainingFillableMakerAssetAmount[${index}]`, amount),
+ );
+ assert.doesConformToSchema('signedFeeOrders', signedFeeOrders, schemas.signedOrdersSchema);
+ _.forEach(remainingFillableFeeAmounts, (amount, index) =>
+ assert.isValidBaseUnitAmount(`remainingFillableFeeAmounts[${index}]`, amount),
+ );
+ assert.isValidBaseUnitAmount('slippageBufferAmount', slippageBufferAmount);
+ assert.assert(
+ signedOrders.length === remainingFillableMakerAssetAmounts.length,
+ 'Expected signedOrders.length to equal remainingFillableMakerAssetAmounts.length',
+ );
+ assert.assert(
+ signedOrders.length === remainingFillableMakerAssetAmounts.length,
+ 'Expected signedFeeOrders.length to equal remainingFillableFeeAmounts.length',
+ );
+ // calculate total amount of ZRX needed to fill signedOrders
+ const totalFeeAmount = _.reduce(
+ signedOrders,
+ (accFees, order, index) => {
+ const makerAssetAmountAvailable = remainingFillableMakerAssetAmounts[index];
+ const feeToFillMakerAssetAmountAvailable = makerAssetAmountAvailable
+ .mul(order.takerFee)
+ .div(order.makerAssetAmount);
+ return accFees.plus(feeToFillMakerAssetAmountAvailable);
+ },
+ constants.ZERO_AMOUNT,
+ );
+ const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
+ signedFeeOrders,
+ remainingFillableFeeAmounts,
+ totalFeeAmount,
+ slippageBufferAmount,
+ );
+ return {
+ resultOrders,
+ remainingFeeAmount: remainingFillAmount,
+ };
+ // TODO: add more orders here to cover rounding
+ // https://github.com/0xProject/0x-protocol-specification/blob/master/v2/forwarding-contract-specification.md#over-buying-zrx
+ },
+};
diff --git a/packages/order-utils/src/order_factory.ts b/packages/order-utils/src/order_factory.ts
index 14122f353..46a69ae4d 100644
--- a/packages/order-utils/src/order_factory.ts
+++ b/packages/order-utils/src/order_factory.ts
@@ -1,75 +1,90 @@
-import { ECSignature, SignedOrder } from '@0xproject/types';
+import { Order, SignedOrder, SignerType } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
import { Provider } from 'ethereum-types';
-import * as ethUtil from 'ethereumjs-util';
import * as _ from 'lodash';
+import { constants } from './constants';
import { orderHashUtils } from './order_hash';
import { generatePseudoRandomSalt } from './salt';
import { signatureUtils } from './signature_utils';
-import { MessagePrefixType } from './types';
+import { CreateOrderOpts } from './types';
export const orderFactory = {
- async createSignedOrderAsync(
- provider: Provider,
+ createOrder(
makerAddress: string,
- takerAddress: string,
- senderAddress: string,
- makerFee: BigNumber,
- takerFee: BigNumber,
makerAssetAmount: BigNumber,
makerAssetData: string,
takerAssetAmount: BigNumber,
takerAssetData: string,
exchangeAddress: string,
- feeRecipientAddress: string,
- expirationTimeSecondsIfExists?: BigNumber,
- ): Promise<SignedOrder> {
- const defaultExpirationUnixTimestampSec = new BigNumber(2524604400); // Close to infinite
- const expirationTimeSeconds = _.isUndefined(expirationTimeSecondsIfExists)
- ? defaultExpirationUnixTimestampSec
- : expirationTimeSecondsIfExists;
+ createOrderOpts: CreateOrderOpts = generateDefaultCreateOrderOpts(),
+ ): Order {
+ const defaultCreateOrderOpts = generateDefaultCreateOrderOpts();
const order = {
makerAddress,
- takerAddress,
- senderAddress,
- makerFee,
- takerFee,
makerAssetAmount,
takerAssetAmount,
makerAssetData,
takerAssetData,
- salt: generatePseudoRandomSalt(),
exchangeAddress,
- feeRecipientAddress,
- expirationTimeSeconds,
+ takerAddress: createOrderOpts.takerAddress || defaultCreateOrderOpts.takerAddress,
+ senderAddress: createOrderOpts.senderAddress || defaultCreateOrderOpts.senderAddress,
+ makerFee: createOrderOpts.makerFee || defaultCreateOrderOpts.makerFee,
+ takerFee: createOrderOpts.takerFee || defaultCreateOrderOpts.takerFee,
+ feeRecipientAddress: createOrderOpts.feeRecipientAddress || defaultCreateOrderOpts.feeRecipientAddress,
+ salt: createOrderOpts.salt || defaultCreateOrderOpts.salt,
+ expirationTimeSeconds:
+ createOrderOpts.expirationTimeSeconds || defaultCreateOrderOpts.expirationTimeSeconds,
};
+ return order;
+ },
+ async createSignedOrderAsync(
+ provider: Provider,
+ makerAddress: string,
+ makerAssetAmount: BigNumber,
+ makerAssetData: string,
+ takerAssetAmount: BigNumber,
+ takerAssetData: string,
+ exchangeAddress: string,
+ createOrderOpts?: CreateOrderOpts,
+ ): Promise<SignedOrder> {
+ const order = orderFactory.createOrder(
+ makerAddress,
+ makerAssetAmount,
+ makerAssetData,
+ takerAssetAmount,
+ takerAssetData,
+ exchangeAddress,
+ createOrderOpts,
+ );
const orderHash = orderHashUtils.getOrderHashHex(order);
- const messagePrefixOpts = {
- prefixType: MessagePrefixType.EthSign,
- shouldAddPrefixBeforeCallingEthSign: false,
- };
- const ecSignature = await signatureUtils.ecSignOrderHashAsync(
+ const signature = await signatureUtils.ecSignOrderHashAsync(
provider,
orderHash,
makerAddress,
- messagePrefixOpts,
+ SignerType.Default,
);
- const signature = getVRSHexString(ecSignature);
const signedOrder: SignedOrder = _.assign(order, { signature });
return signedOrder;
},
};
-function getVRSHexString(ecSignature: ECSignature): string {
- const ETH_SIGN_SIGNATURE_TYPE = '03';
- const vrs = `${intToHex(ecSignature.v)}${ethUtil.stripHexPrefix(ecSignature.r)}${ethUtil.stripHexPrefix(
- ecSignature.s,
- )}${ETH_SIGN_SIGNATURE_TYPE}`;
- return vrs;
-}
-
-function intToHex(i: number): string {
- const hex = ethUtil.bufferToHex(ethUtil.toBuffer(i));
- return hex;
+function generateDefaultCreateOrderOpts(): {
+ takerAddress: string;
+ senderAddress: string;
+ makerFee: BigNumber;
+ takerFee: BigNumber;
+ feeRecipientAddress: string;
+ salt: BigNumber;
+ expirationTimeSeconds: BigNumber;
+} {
+ return {
+ takerAddress: constants.NULL_ADDRESS,
+ senderAddress: constants.NULL_ADDRESS,
+ makerFee: constants.ZERO_AMOUNT,
+ takerFee: constants.ZERO_AMOUNT,
+ feeRecipientAddress: constants.NULL_ADDRESS,
+ salt: generatePseudoRandomSalt(),
+ expirationTimeSeconds: constants.INFINITE_TIMESTAMP_SEC,
+ };
}
diff --git a/packages/order-utils/src/signature_utils.ts b/packages/order-utils/src/signature_utils.ts
index 2dc465256..d466bb00d 100644
--- a/packages/order-utils/src/signature_utils.ts
+++ b/packages/order-utils/src/signature_utils.ts
@@ -1,5 +1,5 @@
import { schemas } from '@0xproject/json-schemas';
-import { ECSignature, SignatureType, ValidatorSignature } from '@0xproject/types';
+import { ECSignature, SignatureType, SignerType, ValidatorSignature } from '@0xproject/types';
import { Web3Wrapper } from '@0xproject/web3-wrapper';
import { Provider } from 'ethereum-types';
import * as ethUtil from 'ethereumjs-util';
@@ -10,13 +10,12 @@ import { assert } from './assert';
import { ExchangeContract } from './generated_contract_wrappers/exchange';
import { IValidatorContract } from './generated_contract_wrappers/i_validator';
import { IWalletContract } from './generated_contract_wrappers/i_wallet';
-import { MessagePrefixOpts, MessagePrefixType, OrderError } from './types';
+import { OrderError } from './types';
import { utils } from './utils';
export const signatureUtils = {
/**
* Verifies that the provided signature is valid according to the 0x Protocol smart contracts
- * @param provider Web3 provider to use for all JSON RPC requests
* @param data The hex encoded data signed by the supplied signature.
* @param signature A hex encoded 0x Protocol signature made up of: [TypeSpecificData][SignatureType].
* E.g [vrs][SignatureType.EIP712]
@@ -50,7 +49,7 @@ export const signatureUtils = {
case SignatureType.EthSign: {
const ecSignature = signatureUtils.parseECSignature(signature);
- const prefixedMessageHex = signatureUtils.addSignedMessagePrefix(data, MessagePrefixType.EthSign);
+ const prefixedMessageHex = signatureUtils.addSignedMessagePrefix(data, SignerType.EthSign);
return signatureUtils.isValidECSignature(prefixedMessageHex, ecSignature, signerAddress);
}
@@ -84,7 +83,7 @@ export const signatureUtils = {
}
case SignatureType.Trezor: {
- const prefixedMessageHex = signatureUtils.addSignedMessagePrefix(data, MessagePrefixType.Trezor);
+ const prefixedMessageHex = signatureUtils.addSignedMessagePrefix(data, SignerType.Trezor);
const ecSignature = signatureUtils.parseECSignature(signature);
return signatureUtils.isValidECSignature(prefixedMessageHex, ecSignature, signerAddress);
}
@@ -202,23 +201,21 @@ export const signatureUtils = {
}
},
/**
- * Signs an orderHash and returns it's elliptic curve signature.
+ * Signs an orderHash and returns it's elliptic curve signature and signature type.
* This method currently supports TestRPC, Geth and Parity above and below V1.6.6
- * @param provider The provider to use for JSON RPC calls
- * @param orderHash Hex encoded orderHash to sign.
- * @param signerAddress The hex encoded Ethereum address you wish to sign it with. This address
+ * @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 messagePrefixOpts Different signers add/require different prefixes be appended to the message being signed.
- * Since we cannot know ahead of time which signer you are using, you must supply both a prefixType and
- * whether it must be added before calling `eth_sign` (some signers add it themselves)
- * @return An object containing the Elliptic curve signature parameters generated by signing the orderHash.
+ * @param signerType Different signers add/require different prefixes to be prepended to the message being signed.
+ * Since we cannot know ahead of time which signer you are using, you must supply a SignerType.
+ * @return A hex encoded string containing the Elliptic curve signature generated by signing the orderHash and the Signature Type.
*/
async ecSignOrderHashAsync(
provider: Provider,
orderHash: string,
signerAddress: string,
- messagePrefixOpts: MessagePrefixOpts,
- ): Promise<ECSignature> {
+ signerType: SignerType,
+ ): Promise<string> {
assert.isWeb3Provider('provider', provider);
assert.isHexString('orderHash', orderHash);
assert.isETHAddressHex('signerAddress', signerAddress);
@@ -227,8 +224,10 @@ export const signatureUtils = {
const normalizedSignerAddress = signerAddress.toLowerCase();
let msgHashHex = orderHash;
- const prefixedMsgHashHex = signatureUtils.addSignedMessagePrefix(orderHash, messagePrefixOpts.prefixType);
- if (messagePrefixOpts.shouldAddPrefixBeforeCallingEthSign) {
+ const prefixedMsgHashHex = signatureUtils.addSignedMessagePrefix(orderHash, signerType);
+ // Metamask incorrectly implements eth_sign and does not prefix the message as per the spec
+ // Source: https://github.com/MetaMask/metamask-extension/commit/a9d36860bec424dcee8db043d3e7da6a5ff5672e
+ if (signerType === SignerType.Metamask) {
msgHashHex = prefixedMsgHashHex;
}
const signature = await web3Wrapper.signMessageAsync(normalizedSignerAddress, msgHashHex);
@@ -237,8 +236,24 @@ export const signatureUtils = {
// 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.
+ // r + s + v is the most prevalent format from eth_sign, so we attempt this first.
// tslint:disable-next-line:custom-no-magic-numbers
const validVParamValues = [27, 28];
+ const ecSignatureRSV = parseSignatureHexAsRSV(signature);
+ if (_.includes(validVParamValues, ecSignatureRSV.v)) {
+ const isValidRSVSignature = signatureUtils.isValidECSignature(
+ prefixedMsgHashHex,
+ ecSignatureRSV,
+ normalizedSignerAddress,
+ );
+ if (isValidRSVSignature) {
+ const convertedSignatureHex = signatureUtils.convertECSignatureToSignatureHex(
+ ecSignatureRSV,
+ signerType,
+ );
+ return convertedSignatureHex;
+ }
+ }
const ecSignatureVRS = parseSignatureHexAsVRS(signature);
if (_.includes(validVParamValues, ecSignatureVRS.v)) {
const isValidVRSSignature = signatureUtils.isValidECSignature(
@@ -247,54 +262,85 @@ export const signatureUtils = {
normalizedSignerAddress,
);
if (isValidVRSSignature) {
- return ecSignatureVRS;
+ const convertedSignatureHex = signatureUtils.convertECSignatureToSignatureHex(
+ ecSignatureVRS,
+ signerType,
+ );
+ return convertedSignatureHex;
}
}
- const ecSignatureRSV = parseSignatureHexAsRSV(signature);
- if (_.includes(validVParamValues, ecSignatureRSV.v)) {
- const isValidRSVSignature = signatureUtils.isValidECSignature(
- prefixedMsgHashHex,
- ecSignatureRSV,
- normalizedSignerAddress,
- );
- if (isValidRSVSignature) {
- return ecSignatureRSV;
+ throw new Error(OrderError.InvalidSignature);
+ },
+ /**
+ * Combines ECSignature with V,R,S and the relevant signature type for use in 0x protocol
+ * @param ecSignature The ECSignature of the signed data
+ * @param signerType The SignerType of the signed data
+ * @return Hex encoded string of signature (v,r,s) with Signature Type
+ */
+ convertECSignatureToSignatureHex(ecSignature: ECSignature, signerType: SignerType): string {
+ const signatureBuffer = Buffer.concat([
+ ethUtil.toBuffer(ecSignature.v),
+ ethUtil.toBuffer(ecSignature.r),
+ ethUtil.toBuffer(ecSignature.s),
+ ]);
+ const signatureHex = `0x${signatureBuffer.toString('hex')}`;
+ let signatureType;
+ switch (signerType) {
+ case SignerType.Metamask:
+ case SignerType.Ledger:
+ case SignerType.Default: {
+ signatureType = SignatureType.EthSign;
+ break;
}
+ case SignerType.Trezor: {
+ signatureType = SignatureType.Trezor;
+ break;
+ }
+ default:
+ throw new Error(`Unrecognized SignerType: ${signerType}`);
}
-
- throw new Error(OrderError.InvalidSignature);
+ const signatureWithType = signatureUtils.convertToSignatureWithType(signatureHex, signatureType);
+ return signatureWithType;
+ },
+ /**
+ * Combines the signature proof and the Signature Type.
+ * @param signature The hex encoded signature proof
+ * @param signatureType The signature type, i.e EthSign, Trezor, Wallet etc.
+ * @return Hex encoded string of signature proof with Signature Type
+ */
+ convertToSignatureWithType(signature: string, signatureType: SignatureType): string {
+ const signatureBuffer = Buffer.concat([ethUtil.toBuffer(signature), ethUtil.toBuffer(signatureType)]);
+ const signatureHex = `0x${signatureBuffer.toString('hex')}`;
+ return signatureHex;
},
/**
* Adds the relevant prefix to the message being signed.
* @param message Message to sign
- * @param messagePrefixType The type of message prefix to add. Different signers expect
+ * @param signerType The type of message prefix to add for a given SignerType. Different signers expect
* specific message prefixes.
* @return Prefixed message
*/
- addSignedMessagePrefix(message: string, messagePrefixType: MessagePrefixType): string {
+ addSignedMessagePrefix(message: string, signerType: SignerType = SignerType.Default): string {
assert.isString('message', message);
- assert.doesBelongToStringEnum('messagePrefixType', messagePrefixType, MessagePrefixType);
- switch (messagePrefixType) {
- case MessagePrefixType.None:
- return message;
-
- case MessagePrefixType.EthSign: {
+ assert.doesBelongToStringEnum('signerType', signerType, SignerType);
+ switch (signerType) {
+ case SignerType.Metamask:
+ case SignerType.Ledger:
+ case SignerType.Default: {
const msgBuff = ethUtil.toBuffer(message);
const prefixedMsgBuff = ethUtil.hashPersonalMessage(msgBuff);
const prefixedMsgHex = ethUtil.bufferToHex(prefixedMsgBuff);
return prefixedMsgHex;
}
-
- case MessagePrefixType.Trezor: {
+ case SignerType.Trezor: {
const msgBuff = ethUtil.toBuffer(message);
const prefixedMsgBuff = hashTrezorPersonalMessage(msgBuff);
const prefixedMsgHex = ethUtil.bufferToHex(prefixedMsgBuff);
return prefixedMsgHex;
}
-
default:
- throw new Error(`Unrecognized MessagePrefixType: ${messagePrefixType}`);
+ throw new Error(`Unrecognized SignerType: ${signerType}`);
}
},
/**
diff --git a/packages/order-utils/src/types.ts b/packages/order-utils/src/types.ts
index b08e74e71..1fbd8cf7b 100644
--- a/packages/order-utils/src/types.ts
+++ b/packages/order-utils/src/types.ts
@@ -1,29 +1,9 @@
+import { BigNumber } from '@0xproject/utils';
+
export enum OrderError {
InvalidSignature = 'INVALID_SIGNATURE',
}
-/**
- * The requisite message prefix (is any) to add to an `eth_sign` request.
- */
-export enum MessagePrefixType {
- None = 'NONE',
- EthSign = 'ETH_SIGN',
- Trezor = 'TREZOR',
-}
-
-/**
- * Options related to message prefixing of messages sent to `eth_sign`
- * Some signers prepend a message prefix (e.g Parity Signer, Ledger, TestRPC), while
- * others require it already be prepended (e.g Metamask). In addition, different signers
- * expect slightly different prefixes (See: https://github.com/ethereum/go-ethereum/issues/14794).
- * Depending on the signer that will receive your signing request, you must specify the
- * desired prefix and whether it should be added before making the `eth_sign` request.
- */
-export interface MessagePrefixOpts {
- prefixType: MessagePrefixType;
- shouldAddPrefixBeforeCallingEthSign: boolean;
-}
-
export enum TradeSide {
Maker = 'maker',
Taker = 'taker',
@@ -51,3 +31,13 @@ export enum EIP712Types {
String = 'string',
Uint256 = 'uint256',
}
+
+export interface CreateOrderOpts {
+ takerAddress?: string;
+ senderAddress?: string;
+ makerFee?: BigNumber;
+ takerFee?: BigNumber;
+ feeRecipientAddress?: string;
+ salt?: BigNumber;
+ expirationTimeSeconds?: BigNumber;
+}
diff --git a/packages/order-utils/test/market_utils_test.ts b/packages/order-utils/test/market_utils_test.ts
new file mode 100644
index 000000000..21c0a4802
--- /dev/null
+++ b/packages/order-utils/test/market_utils_test.ts
@@ -0,0 +1,280 @@
+import { BigNumber } from '@0xproject/utils';
+import * as chai from 'chai';
+import 'mocha';
+
+import { constants, marketUtils } from '../src';
+
+import { chaiSetup } from './utils/chai_setup';
+import { testOrderFactory } from './utils/test_order_factory';
+
+chaiSetup.configure();
+const expect = chai.expect;
+
+// tslint:disable: no-unused-expression
+describe('marketUtils', () => {
+ describe('#findOrdersThatCoverMakerAssetFillAmount', () => {
+ describe('no orders', () => {
+ it('returns empty and unchanged remainingFillAmount', async () => {
+ const fillAmount = new BigNumber(10);
+ const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
+ [],
+ [],
+ fillAmount,
+ );
+ expect(resultOrders).to.be.empty;
+ expect(remainingFillAmount).to.be.bignumber.equal(fillAmount);
+ });
+ });
+ describe('orders are completely fillable', () => {
+ // generate three signed orders each with 10 units of makerAsset, 30 total
+ const makerAssetAmount = new BigNumber(10);
+ const inputOrders = testOrderFactory.generateTestSignedOrders(
+ {
+ makerAssetAmount,
+ },
+ 3,
+ );
+ // generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount
+ const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount];
+ it('returns input orders and zero remainingFillAmount when input exactly matches requested fill amount', async () => {
+ // try to fill 20 units of makerAsset
+ // include 10 units of slippageBufferAmount
+ const fillAmount = new BigNumber(20);
+ const slippageBufferAmount = new BigNumber(10);
+ const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
+ inputOrders,
+ remainingFillableMakerAssetAmounts,
+ fillAmount,
+ slippageBufferAmount,
+ );
+ expect(resultOrders).to.be.deep.equal(inputOrders);
+ expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
+ });
+ it('returns input orders and zero remainingFillAmount when input has more than requested fill amount', async () => {
+ // try to fill 15 units of makerAsset
+ // include 10 units of slippageBufferAmount
+ const fillAmount = new BigNumber(15);
+ const slippageBufferAmount = new BigNumber(10);
+ const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
+ inputOrders,
+ remainingFillableMakerAssetAmounts,
+ fillAmount,
+ slippageBufferAmount,
+ );
+ expect(resultOrders).to.be.deep.equal(inputOrders);
+ expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
+ });
+ it('returns input orders and non-zero remainingFillAmount when input has less than requested fill amount', async () => {
+ // try to fill 30 units of makerAsset
+ // include 5 units of slippageBufferAmount
+ const fillAmount = new BigNumber(30);
+ const slippageBufferAmount = new BigNumber(5);
+ const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
+ inputOrders,
+ remainingFillableMakerAssetAmounts,
+ fillAmount,
+ slippageBufferAmount,
+ );
+ expect(resultOrders).to.be.deep.equal(inputOrders);
+ expect(remainingFillAmount).to.be.bignumber.equal(new BigNumber(5));
+ });
+ it('returns first order and zero remainingFillAmount when requested fill amount is exactly covered by the first order', async () => {
+ // try to fill 10 units of makerAsset
+ const fillAmount = new BigNumber(10);
+ const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
+ inputOrders,
+ remainingFillableMakerAssetAmounts,
+ fillAmount,
+ );
+ expect(resultOrders).to.be.deep.equal([inputOrders[0]]);
+ expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
+ });
+ it('returns first two orders and zero remainingFillAmount when requested fill amount is over covered by the first two order', async () => {
+ // try to fill 15 units of makerAsset
+ const fillAmount = new BigNumber(15);
+ const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
+ inputOrders,
+ remainingFillableMakerAssetAmounts,
+ fillAmount,
+ );
+ expect(resultOrders).to.be.deep.equal([inputOrders[0], inputOrders[1]]);
+ expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
+ });
+ });
+ describe('orders are partially fillable', () => {
+ // generate three signed orders each with 10 units of makerAsset, 30 total
+ const makerAssetAmount = new BigNumber(10);
+ const inputOrders = testOrderFactory.generateTestSignedOrders(
+ {
+ makerAssetAmount,
+ },
+ 3,
+ );
+ // generate remainingFillableMakerAssetAmounts that cover different partial fill scenarios
+ // 1. order is completely filled already
+ // 2. order is partially fillable
+ // 3. order is completely fillable
+ const remainingFillableMakerAssetAmounts = [constants.ZERO_AMOUNT, new BigNumber(5), makerAssetAmount];
+ it('returns last two orders and non-zero remainingFillAmount when trying to fill original makerAssetAmounts', async () => {
+ // try to fill 30 units of makerAsset
+ const fillAmount = new BigNumber(30);
+ const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
+ inputOrders,
+ remainingFillableMakerAssetAmounts,
+ fillAmount,
+ );
+ expect(resultOrders).to.be.deep.equal([inputOrders[1], inputOrders[2]]);
+ expect(remainingFillAmount).to.be.bignumber.equal(new BigNumber(15));
+ });
+ });
+ });
+ describe('#findFeeOrdersThatCoverFeesForTargetOrders', () => {
+ // generate three signed fee orders each with 10 units of ZRX, 30 total
+ const zrxAmount = new BigNumber(10);
+ const inputFeeOrders = testOrderFactory.generateTestSignedOrders(
+ {
+ makerAssetAmount: zrxAmount,
+ },
+ 3,
+ );
+ // generate remainingFillableFeeAmounts that equal the zrxAmount
+ const remainingFillableFeeAmounts = [zrxAmount, zrxAmount, zrxAmount];
+ describe('no target orders', () => {
+ it('returns empty and zero remainingFeeAmount', async () => {
+ const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders(
+ [],
+ [],
+ inputFeeOrders,
+ remainingFillableFeeAmounts,
+ );
+ expect(resultOrders).to.be.empty;
+ expect(remainingFeeAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
+ });
+ });
+ describe('no fee orders', () => {
+ // generate three signed orders each with 10 units of makerAsset, 30 total
+ // each signed order requires 10 units of takerFee
+ const makerAssetAmount = new BigNumber(10);
+ const takerFee = new BigNumber(10);
+ const inputOrders = testOrderFactory.generateTestSignedOrders(
+ {
+ makerAssetAmount,
+ takerFee,
+ },
+ 3,
+ );
+ // generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount
+ const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount];
+ it('returns empty and non-zero remainingFeeAmount', async () => {
+ const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders(
+ inputOrders,
+ remainingFillableMakerAssetAmounts,
+ [],
+ [],
+ );
+ expect(resultOrders).to.be.empty;
+ expect(remainingFeeAmount).to.be.bignumber.equal(new BigNumber(30));
+ });
+ });
+ describe('target orders have no fees', () => {
+ // generate three signed orders each with 10 units of makerAsset, 30 total
+ const makerAssetAmount = new BigNumber(10);
+ const inputOrders = testOrderFactory.generateTestSignedOrders(
+ {
+ makerAssetAmount,
+ },
+ 3,
+ );
+ // generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount
+ const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount];
+ it('returns empty and zero remainingFeeAmount', async () => {
+ const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders(
+ inputOrders,
+ remainingFillableMakerAssetAmounts,
+ inputFeeOrders,
+ remainingFillableFeeAmounts,
+ );
+ expect(resultOrders).to.be.empty;
+ expect(remainingFeeAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
+ });
+ });
+ describe('target orders require fees and are completely fillable', () => {
+ // generate three signed orders each with 10 units of makerAsset, 30 total
+ // each signed order requires 10 units of takerFee
+ const makerAssetAmount = new BigNumber(10);
+ const takerFee = new BigNumber(10);
+ const inputOrders = testOrderFactory.generateTestSignedOrders(
+ {
+ makerAssetAmount,
+ takerFee,
+ },
+ 3,
+ );
+ // generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount
+ const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount];
+ it('returns input fee orders and zero remainingFeeAmount', async () => {
+ const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders(
+ inputOrders,
+ remainingFillableMakerAssetAmounts,
+ inputFeeOrders,
+ remainingFillableFeeAmounts,
+ );
+ expect(resultOrders).to.be.deep.equal(inputFeeOrders);
+ expect(remainingFeeAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
+ });
+ });
+ describe('target orders require fees and are partially fillable', () => {
+ // generate three signed orders each with 10 units of makerAsset, 30 total
+ // each signed order requires 10 units of takerFee
+ const makerAssetAmount = new BigNumber(10);
+ const takerFee = new BigNumber(10);
+ const inputOrders = testOrderFactory.generateTestSignedOrders(
+ {
+ makerAssetAmount,
+ takerFee,
+ },
+ 3,
+ );
+ // generate remainingFillableMakerAssetAmounts that cover different partial fill scenarios
+ // 1. order is completely filled already
+ // 2. order is partially fillable
+ // 3. order is completely fillable
+ const remainingFillableMakerAssetAmounts = [constants.ZERO_AMOUNT, new BigNumber(5), makerAssetAmount];
+ it('returns first two input fee orders and zero remainingFeeAmount', async () => {
+ const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders(
+ inputOrders,
+ remainingFillableMakerAssetAmounts,
+ inputFeeOrders,
+ remainingFillableFeeAmounts,
+ );
+ expect(resultOrders).to.be.deep.equal([inputFeeOrders[0], inputFeeOrders[1]]);
+ expect(remainingFeeAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
+ });
+ });
+ describe('target orders require more fees than available', () => {
+ // generate three signed orders each with 10 units of makerAsset, 30 total
+ // each signed order requires 20 units of takerFee
+ const makerAssetAmount = new BigNumber(10);
+ const takerFee = new BigNumber(20);
+ const inputOrders = testOrderFactory.generateTestSignedOrders(
+ {
+ makerAssetAmount,
+ takerFee,
+ },
+ 3,
+ );
+ // generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount
+ const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount];
+ it('returns input fee orders and non-zero remainingFeeAmount', async () => {
+ const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders(
+ inputOrders,
+ remainingFillableMakerAssetAmounts,
+ inputFeeOrders,
+ remainingFillableFeeAmounts,
+ );
+ expect(resultOrders).to.be.deep.equal(inputFeeOrders);
+ expect(remainingFeeAmount).to.be.bignumber.equal(new BigNumber(30));
+ });
+ });
+ });
+});
diff --git a/packages/order-utils/test/signature_utils_test.ts b/packages/order-utils/test/signature_utils_test.ts
index 2f36386ad..06ff86515 100644
--- a/packages/order-utils/test/signature_utils_test.ts
+++ b/packages/order-utils/test/signature_utils_test.ts
@@ -1,3 +1,4 @@
+import { SignerType } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
import * as chai from 'chai';
import { JSONRPCErrorCallback, JSONRPCRequestPayload } from 'ethereum-types';
@@ -5,8 +6,8 @@ import * as _ from 'lodash';
import 'mocha';
import * as Sinon from 'sinon';
-import { generatePseudoRandomSalt, MessagePrefixType } from '../src';
-import { signatureUtils } from '../src/signature_utils';
+import { signatureUtils, generatePseudoRandomSalt } from '../src';
+import { convertECSignatureToSignatureHex, isValidSignatureAsync } from '../src/signature_utils';
import { chaiSetup } from './utils/chai_setup';
import { provider, web3Wrapper } from './utils/web3_wrapper';
@@ -22,8 +23,9 @@ describe('Signature utils', () => {
let address = '0x5409ed021d9299bf6814279a6a1411a7e866a631';
it("should return false if the data doesn't pertain to the signature & address", async () => {
+ const bytes32Zeros = '0x0000000000000000000000000000000000000000000000000000000000000000';
expect(
- await signatureUtils.isValidSignatureAsync(provider, '0x0', ethSignSignature, address),
+ await signatureUtils.isValidSignatureAsync(provider, bytes32Zeros, ethSignSignature, address),
).to.be.false();
});
it("should return false if the address doesn't pertain to the signature & data", async () => {
@@ -139,37 +141,28 @@ describe('Signature utils', () => {
_.each(stubs, s => s.restore());
stubs = [];
});
- it('Should return the correct ECSignature', async () => {
+ it('Should return the correct Signature', async () => {
const orderHash = '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0';
- const expectedECSignature = {
- v: 27,
- r: '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33',
- s: '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254',
- };
- const messagePrefixOpts = {
- prefixType: MessagePrefixType.EthSign,
- shouldAddPrefixBeforeCallingEthSign: false,
- };
+ const expectedSignature =
+ '0x1b61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc3340349190569279751135161d22529dc25add4f6069af05be04cacbda2ace225403';
const ecSignature = await signatureUtils.ecSignOrderHashAsync(
provider,
orderHash,
makerAddress,
- messagePrefixOpts,
+ SignerType.Default,
);
- expect(ecSignature).to.deep.equal(expectedECSignature);
+ expect(ecSignature).to.equal(expectedSignature);
});
- it('should return the correct ECSignature for signatureHex concatenated as R + S + V', async () => {
+ it('should return the correct Signature for signatureHex concatenated as R + S + V', async () => {
const orderHash = '0x34decbedc118904df65f379a175bb39ca18209d6ce41d5ed549d54e6e0a95004';
- const expectedECSignature = {
- v: 27,
- r: '0x117902c86dfb95fe0d1badd983ee166ad259b27acb220174cbb4460d87287113',
- s: '0x7feabdfe76e05924b484789f79af4ee7fa29ec006cedce1bbf369320d034e10b',
- };
+ const expectedSignature =
+ '0x1b117902c86dfb95fe0d1badd983ee166ad259b27acb220174cbb4460d872871137feabdfe76e05924b484789f79af4ee7fa29ec006cedce1bbf369320d034e10b03';
const fakeProvider = {
async sendAsync(payload: JSONRPCRequestPayload, callback: JSONRPCErrorCallback): Promise<void> {
if (payload.method === 'eth_sign') {
const [address, message] = payload.params;
+ expect(message).to.equal(orderHash);
const signature = await web3Wrapper.signMessageAsync(address, message);
// tslint:disable-next-line:custom-no-magic-numbers
const rsvHex = `0x${signature.substr(130)}${signature.substr(2, 128)}`;
@@ -183,26 +176,18 @@ describe('Signature utils', () => {
}
},
};
-
- const messagePrefixOpts = {
- prefixType: MessagePrefixType.EthSign,
- shouldAddPrefixBeforeCallingEthSign: false,
- };
const ecSignature = await signatureUtils.ecSignOrderHashAsync(
fakeProvider,
orderHash,
makerAddress,
- messagePrefixOpts,
+ SignerType.Default,
);
- expect(ecSignature).to.deep.equal(expectedECSignature);
+ expect(ecSignature).to.equal(expectedSignature);
});
- it('should return the correct ECSignature for signatureHex concatenated as V + R + S', async () => {
+ it('should return the correct Signature for signatureHex concatenated as V + R + S', async () => {
const orderHash = '0x34decbedc118904df65f379a175bb39ca18209d6ce41d5ed549d54e6e0a95004';
- const expectedECSignature = {
- v: 27,
- r: '0x117902c86dfb95fe0d1badd983ee166ad259b27acb220174cbb4460d87287113',
- s: '0x7feabdfe76e05924b484789f79af4ee7fa29ec006cedce1bbf369320d034e10b',
- };
+ const expectedSignature =
+ '0x1b117902c86dfb95fe0d1badd983ee166ad259b27acb220174cbb4460d872871137feabdfe76e05924b484789f79af4ee7fa29ec006cedce1bbf369320d034e10b03';
const fakeProvider = {
async sendAsync(payload: JSONRPCRequestPayload, callback: JSONRPCErrorCallback): Promise<void> {
if (payload.method === 'eth_sign') {
@@ -219,17 +204,96 @@ describe('Signature utils', () => {
},
};
- const messagePrefixOpts = {
- prefixType: MessagePrefixType.EthSign,
- shouldAddPrefixBeforeCallingEthSign: false,
+ const ecSignature = await signatureUtils.ecSignOrderHashAsync(
+ fakeProvider,
+ orderHash,
+ makerAddress,
+ SignerType.Default,
+ );
+ expect(ecSignature).to.equal(expectedSignature);
+ });
+ // Note this is due to a bug in Metamask where it does not prefix before signing, this is a known issue and is to be fixed in the future
+ // Source: https://github.com/MetaMask/metamask-extension/commit/a9d36860bec424dcee8db043d3e7da6a5ff5672e
+ it('should receive a payload modified with a prefix when Metamask is SignerType', async () => {
+ const orderHash = '0x34decbedc118904df65f379a175bb39ca18209d6ce41d5ed549d54e6e0a95004';
+ const orderHashPrefixed = '0xae70f31d26096291aa681b26cb7574563956221d0b4213631e1ef9df675d4cba';
+ const expectedSignature =
+ '0x1b117902c86dfb95fe0d1badd983ee166ad259b27acb220174cbb4460d872871137feabdfe76e05924b484789f79af4ee7fa29ec006cedce1bbf369320d034e10b03';
+ // Generated from a MM eth_sign request from 0x5409ed021d9299bf6814279a6a1411a7e866a631 signing 0xae70f31d26096291aa681b26cb7574563956221d0b4213631e1ef9df675d4cba
+ const metamaskSignature =
+ '0x117902c86dfb95fe0d1badd983ee166ad259b27acb220174cbb4460d872871137feabdfe76e05924b484789f79af4ee7fa29ec006cedce1bbf369320d034e10b1b';
+ const fakeProvider = {
+ async sendAsync(payload: JSONRPCRequestPayload, callback: JSONRPCErrorCallback): Promise<void> {
+ if (payload.method === 'eth_sign') {
+ const [, message] = payload.params;
+ expect(message).to.equal(orderHashPrefixed);
+ callback(null, {
+ id: 42,
+ jsonrpc: '2.0',
+ result: metamaskSignature,
+ });
+ } else {
+ callback(null, { id: 42, jsonrpc: '2.0', result: [makerAddress] });
+ }
+ },
};
+
const ecSignature = await signatureUtils.ecSignOrderHashAsync(
fakeProvider,
orderHash,
makerAddress,
- messagePrefixOpts,
+ SignerType.Metamask,
+ );
+ expect(ecSignature).to.equal(expectedSignature);
+ });
+ it('should return a valid signature', async () => {
+ const orderHash = '0x34decbedc118904df65f379a175bb39ca18209d6ce41d5ed549d54e6e0a95004';
+ const ecSignature = await signatureUtils.ecSignOrderHashAsync(
+ provider,
+ orderHash,
+ makerAddress,
+ SignerType.Default,
+ );
+
+ const isValidSignature = await isValidSignatureAsync(provider, orderHash, ecSignature, makerAddress);
+ expect(isValidSignature).to.be.true();
+ });
+ });
+ describe('#convertECSignatureToSignatureHex', () => {
+ const ecSignature: ECSignature = {
+ v: 27,
+ r: '0xaca7da997ad177f040240cdccf6905b71ab16b74434388c3a72f34fd25d64393',
+ s: '0x46b2bac274ff29b48b3ea6e2d04c1336eaceafda3c53ab483fc3ff12fac3ebf2',
+ };
+ it('should concatenate v,r,s and append the Trezor signature type', async () => {
+ const expectedSignatureWithSignatureType =
+ '0x1baca7da997ad177f040240cdccf6905b71ab16b74434388c3a72f34fd25d6439346b2bac274ff29b48b3ea6e2d04c1336eaceafda3c53ab483fc3ff12fac3ebf208';
+ const signatureWithSignatureType = convertECSignatureToSignatureHex(ecSignature, SignerType.Trezor);
+ expect(signatureWithSignatureType).to.equal(expectedSignatureWithSignatureType);
+ });
+ it('should concatenate v,r,s and append the EthSign signature type when SignerType is Default', async () => {
+ const expectedSignatureWithSignatureType =
+ '0x1baca7da997ad177f040240cdccf6905b71ab16b74434388c3a72f34fd25d6439346b2bac274ff29b48b3ea6e2d04c1336eaceafda3c53ab483fc3ff12fac3ebf203';
+ const signatureWithSignatureType = convertECSignatureToSignatureHex(ecSignature, SignerType.Default);
+ expect(signatureWithSignatureType).to.equal(expectedSignatureWithSignatureType);
+ });
+ it('should concatenate v,r,s and append the EthSign signature type when SignerType is Ledger', async () => {
+ const expectedSignatureWithSignatureType =
+ '0x1baca7da997ad177f040240cdccf6905b71ab16b74434388c3a72f34fd25d6439346b2bac274ff29b48b3ea6e2d04c1336eaceafda3c53ab483fc3ff12fac3ebf203';
+ const signatureWithSignatureType = convertECSignatureToSignatureHex(ecSignature, SignerType.Ledger);
+ expect(signatureWithSignatureType).to.equal(expectedSignatureWithSignatureType);
+ });
+ it('should concatenate v,r,s and append the EthSign signature type when SignerType is Metamask', async () => {
+ const expectedSignatureWithSignatureType =
+ '0x1baca7da997ad177f040240cdccf6905b71ab16b74434388c3a72f34fd25d6439346b2bac274ff29b48b3ea6e2d04c1336eaceafda3c53ab483fc3ff12fac3ebf203';
+ const signatureWithSignatureType = convertECSignatureToSignatureHex(ecSignature, SignerType.Metamask);
+ expect(signatureWithSignatureType).to.equal(expectedSignatureWithSignatureType);
+ });
+ it('should throw if the SignerType is invalid', async () => {
+ const expectedMessage = 'Unrecognized SignerType: INVALID_SIGNER';
+ expect(() => convertECSignatureToSignatureHex(ecSignature, 'INVALID_SIGNER' as SignerType)).to.throw(
+ expectedMessage,
);
- expect(ecSignature).to.deep.equal(expectedECSignature);
});
});
});
diff --git a/packages/order-utils/test/utils/test_order_factory.ts b/packages/order-utils/test/utils/test_order_factory.ts
new file mode 100644
index 000000000..75dc6f1f2
--- /dev/null
+++ b/packages/order-utils/test/utils/test_order_factory.ts
@@ -0,0 +1,32 @@
+import { Order, SignedOrder } from '@0xproject/types';
+import * as _ from 'lodash';
+
+import { constants, orderFactory } from '../../src';
+
+const BASE_TEST_ORDER: Order = orderFactory.createOrder(
+ constants.NULL_ADDRESS,
+ constants.ZERO_AMOUNT,
+ constants.NULL_ADDRESS,
+ constants.ZERO_AMOUNT,
+ constants.NULL_ADDRESS,
+ constants.NULL_ADDRESS,
+);
+const BASE_TEST_SIGNED_ORDER: SignedOrder = {
+ ...BASE_TEST_ORDER,
+ signature: constants.NULL_BYTES,
+};
+
+export const testOrderFactory = {
+ generateTestSignedOrder(partialOrder: Partial<SignedOrder>): SignedOrder {
+ return transformObject(BASE_TEST_SIGNED_ORDER, partialOrder);
+ },
+ generateTestSignedOrders(partialOrder: Partial<SignedOrder>, numOrders: number): SignedOrder[] {
+ const baseTestOrders = _.map(_.range(numOrders), () => BASE_TEST_SIGNED_ORDER);
+ return _.map(baseTestOrders, order => transformObject(order, partialOrder));
+ },
+};
+
+function transformObject<T>(input: T, transformation: Partial<T>): T {
+ const copy = _.cloneDeep(input);
+ return _.assign(copy, transformation);
+}