diff options
Diffstat (limited to 'packages/order-utils')
17 files changed, 681 insertions, 425 deletions
diff --git a/packages/order-utils/package.json b/packages/order-utils/package.json index fa8f3563e..bcfb0611f 100644 --- a/packages/order-utils/package.json +++ b/packages/order-utils/package.json @@ -9,38 +9,29 @@ "types": "lib/src/index.d.ts", "scripts": { "watch_without_deps": "yarn pre_build && tsc -w", - "build": "run-s pre_build transpile copy_monorepo_scripts", - "pre_build": "run-s update_artifacts_v2_beta update_artifacts_v2 generate_contract_wrappers", + "build": "run-s pre_build transpile", + "pre_build": "run-s update_artifacts_v2_beta update_artifacts_v2 copy_artifacts generate_contract_wrappers", "transpile": "tsc", - "copy_monorepo_scripts": "copyfiles -u 3 './lib/src/monorepo_scripts/**/*' ./scripts", - "generate_contract_wrappers": "abi-gen --abis 'lib/src/artifacts/@(Exchange|IWallet|IValidator|DummyERC20Token|ERC20Proxy|ERC20Token).json' --template ../contract_templates/contract.handlebars --partials '../contract_templates/partials/**/*.handlebars' --output src/generated_contract_wrappers --backend ethers", - "update_artifacts_v2_beta": "for i in ${npm_package_config_contracts_v2_beta}; do copyfiles -u 4 ../migrations/artifacts/2.0.0-beta-testnet/$i.json lib/src/artifacts; done;", - "update_artifacts_v2": "for i in ${npm_package_config_contracts_v2}; do copyfiles -u 4 ../migrations/artifacts/2.0.0/$i.json lib/src/artifacts; done;", + "generate_contract_wrappers": "abi-gen --abis './src/artifacts/@(Exchange|IWallet|IValidator|DummyERC20Token|ERC20Proxy|ERC20Token).json' --template ../contract_templates/contract.handlebars --partials '../contract_templates/partials/**/*.handlebars' --output src/generated_contract_wrappers --backend ethers", + "update_artifacts_v2_beta": "for i in ${npm_package_config_contracts_v2_beta}; do copyfiles -u 4 ../migrations/artifacts/2.0.0-beta-testnet/$i.json src/artifacts; done;", + "update_artifacts_v2": "for i in ${npm_package_config_contracts_v2}; do copyfiles -u 4 ../migrations/artifacts/2.0.0/$i.json src/artifacts; done;", + "copy_artifacts": "copyfiles -u 2 './src/artifacts/**/*.json' ./lib/src/artifacts", "test": "yarn run_mocha", "rebuild_and_test": "run-s build test", "test:circleci": "yarn test:coverage", "run_mocha": "mocha --require source-map-support/register --require make-promises-safe lib/test/**/*_test.js --bail --exit", "test:coverage": "nyc npm run test --all && yarn coverage:report:lcov", "coverage:report:lcov": "nyc report --reporter=text-lcov > coverage/lcov.info", - "clean": "shx rm -rf lib scripts lib/src/artifacts src/generated_contract_wrappers", + "clean": "shx rm -rf lib lib/src/artifacts src/generated_contract_wrappers", "lint": "tslint --project . --exclude **/src/generated_contract_wrappers/**/*", - "manual:postpublish": "yarn build; node ./scripts/postpublish.js", - "docs:stage": "node scripts/stage_docs.js", - "docs:json": "typedoc --excludePrivate --excludeExternals --target ES5 --json $JSON_FILE_PATH $PROJECT_FILES", - "upload_docs_json": "aws s3 cp generated_docs/index.json $S3_URL --profile 0xproject --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --content-type application/json" + "docs:json": "typedoc --excludePrivate --excludeExternals --target ES5 --json $JSON_FILE_PATH $PROJECT_FILES" }, "config": { "contracts_v2_beta": "IWallet IValidator Exchange ERC20Proxy ERC20Token", "contracts_v2": "DummyERC20Token", "postpublish": { - "docPublishConfigs": { - "extraFileIncludes": [ - "../types/src/index.ts", - "../ethereum-types/src/index.ts" - ], - "s3BucketPath": "s3://doc-jsons/order-utils/", - "s3StagingBucketPath": "s3://staging-doc-jsons/order-utils/" - } + "assets": [], + "shouldPublishDocs": true } }, "license": "Apache-2.0", @@ -54,7 +45,6 @@ "homepage": "https://github.com/0xProject/0x-monorepo/packages/order-utils/README.md", "devDependencies": { "@0xproject/dev-utils": "^1.0.4", - "@0xproject/monorepo-scripts": "^1.0.5", "@0xproject/tslint-config": "^1.0.5", "@types/bn.js": "^4.11.0", "@types/lodash": "4.14.104", diff --git a/packages/order-utils/src/abstract/abstract_balance_and_proxy_allowance_fetcher.ts b/packages/order-utils/src/abstract/abstract_balance_and_proxy_allowance_fetcher.ts index b2760d98e..7cb859ca7 100644 --- a/packages/order-utils/src/abstract/abstract_balance_and_proxy_allowance_fetcher.ts +++ b/packages/order-utils/src/abstract/abstract_balance_and_proxy_allowance_fetcher.ts @@ -1,6 +1,23 @@ import { BigNumber } from '@0xproject/utils'; +/**l + * An abstract class to be implemented in order to use OrderStateUtils. The class that + * implements this interface must be capable of fetching the balance and proxyAllowance + * for an Ethereum address and assetData + */ export abstract class AbstractBalanceAndProxyAllowanceFetcher { + /** + * Get balance of assetData for userAddress + * @param assetData AssetData for which to fetch the balance + * @param userAddress Ethereum address for which to fetch the balance + * @return Balance amount in base units + */ public abstract async getBalanceAsync(assetData: string, userAddress: string): Promise<BigNumber>; + /** + * Get the 0x asset proxy allowance of assetData for userAddress + * @param assetData AssetData for which to fetch the allowance + * @param userAddress Ethereum address for which to fetch the allowance + * @return Allowance amount in base units + */ public abstract async getProxyAllowanceAsync(assetData: string, userAddress: string): Promise<BigNumber>; } diff --git a/packages/order-utils/src/abstract/abstract_order_filled_cancelled_fetcher.ts b/packages/order-utils/src/abstract/abstract_order_filled_cancelled_fetcher.ts index 865ea4e43..d2b01c359 100644 --- a/packages/order-utils/src/abstract/abstract_order_filled_cancelled_fetcher.ts +++ b/packages/order-utils/src/abstract/abstract_order_filled_cancelled_fetcher.ts @@ -1,7 +1,22 @@ import { BigNumber } from '@0xproject/utils'; +/**l + * An abstract class to be implemented in order to use OrderStateUtils. The class that + * implements this interface must be capable of fetching the amount filled of an order + * and whether it's been cancelled. + */ export abstract class AbstractOrderFilledCancelledFetcher { + /** + * Get the amount of the order's takerToken amount already filled + * @param orderHash OrderHash of order we are interested in + * @return FilledTakerAmount + */ public abstract async getFilledTakerAmountAsync(orderHash: string): Promise<BigNumber>; + /** + * Whether an order is cancelled + * @param orderHash OrderHash of order we are interested in + * @return Whether or not the order is cancelled + */ public abstract async isOrderCancelledAsync(orderHash: string): Promise<boolean>; public abstract getZRXAssetData(): string; } diff --git a/packages/order-utils/src/eip712_utils.ts b/packages/order-utils/src/eip712_utils.ts index 2594e6d6d..b303c93dc 100644 --- a/packages/order-utils/src/eip712_utils.ts +++ b/packages/order-utils/src/eip712_utils.ts @@ -18,14 +18,14 @@ const EIP712_DOMAIN_SCHEMA: EIP712Schema = { ], }; -export const EIP712Utils = { +export const eip712Utils = { /** * Compiles the EIP712Schema and returns the hash of the schema. * @param schema The EIP712 schema. * @return The hash of the compiled schema */ compileSchema(schema: EIP712Schema): Buffer { - const eip712Schema = EIP712Utils._encodeType(schema); + const eip712Schema = eip712Utils._encodeType(schema); const eip712SchemaHashBuffer = crypto.solSHA3([eip712Schema]); return eip712SchemaHashBuffer; }, @@ -36,25 +36,47 @@ export const EIP712Utils = { * @return The hash of an EIP712 message with domain separator prefixed */ createEIP712Message(hashStruct: Buffer, contractAddress: string): Buffer { - const domainSeparatorHashBuffer = EIP712Utils._getDomainSeparatorHashBuffer(contractAddress); + const domainSeparatorHashBuffer = eip712Utils._getDomainSeparatorHashBuffer(contractAddress); const messageBuff = crypto.solSHA3([EIP191_PREFIX, domainSeparatorHashBuffer, hashStruct]); return messageBuff; }, + /** + * Pad an address to 32 bytes + * @param address Address to pad + * @return padded address + */ pad32Address(address: string): Buffer { const addressBuffer = ethUtil.toBuffer(address); - const addressPadded = EIP712Utils.pad32Buffer(addressBuffer); + const addressPadded = eip712Utils.pad32Buffer(addressBuffer); return addressPadded; }, + /** + * Pad an buffer to 32 bytes + * @param buffer Address to pad + * @return padded buffer + */ pad32Buffer(buffer: Buffer): Buffer { const bufferPadded = ethUtil.setLengthLeft(buffer, EIP712_VALUE_LENGTH); return bufferPadded; }, + /** + * Hash together a EIP712 schema with the corresponding data + * @param schema EIP712-compliant schema + * @param data Data the complies to the schema + * @return A buffer containing the SHA256 hash of the schema and encoded data + */ + structHash(schema: EIP712Schema, data: { [key: string]: any }): Buffer { + const encodedData = eip712Utils._encodeData(schema, data); + const schemaHash = eip712Utils.compileSchema(schema); + const hashBuffer = crypto.solSHA3([schemaHash, ...encodedData]); + return hashBuffer; + }, _getDomainSeparatorSchemaBuffer(): Buffer { - return EIP712Utils.compileSchema(EIP712_DOMAIN_SCHEMA); + return eip712Utils.compileSchema(EIP712_DOMAIN_SCHEMA); }, _getDomainSeparatorHashBuffer(exchangeAddress: string): Buffer { - const domainSeparatorSchemaBuffer = EIP712Utils._getDomainSeparatorSchemaBuffer(); - const encodedData = EIP712Utils._encodeData(EIP712_DOMAIN_SCHEMA, { + const domainSeparatorSchemaBuffer = eip712Utils._getDomainSeparatorSchemaBuffer(); + const encodedData = eip712Utils._encodeData(EIP712_DOMAIN_SCHEMA, { name: EIP712_DOMAIN_NAME, version: EIP712_DOMAIN_VERSION, verifyingContract: exchangeAddress, @@ -77,17 +99,11 @@ export const EIP712Utils = { } else if (parameter.type === EIP712Types.Uint256) { encodedValues.push(value); } else if (parameter.type === EIP712Types.Address) { - encodedValues.push(EIP712Utils.pad32Address(value)); + encodedValues.push(eip712Utils.pad32Address(value)); } else { throw new Error(`Unable to encode ${parameter.type}`); } } return encodedValues; }, - structHash(schema: EIP712Schema, data: { [key: string]: any }): Buffer { - const encodedData = EIP712Utils._encodeData(schema, data); - const schemaHash = EIP712Utils.compileSchema(schema); - const hashBuffer = crypto.solSHA3([schemaHash, ...encodedData]); - return hashBuffer; - }, }; diff --git a/packages/order-utils/src/exchange_transfer_simulator.ts b/packages/order-utils/src/exchange_transfer_simulator.ts index c3a4f9c2a..81c849c64 100644 --- a/packages/order-utils/src/exchange_transfer_simulator.ts +++ b/packages/order-utils/src/exchange_transfer_simulator.ts @@ -33,6 +33,10 @@ const ERR_MSG_MAPPING = { }, }; +/** + * An exchange transfer simulator which simulates asset transfers exactly how the + * 0x exchange contract would do them. + */ export class ExchangeTransferSimulator { private readonly _store: AbstractBalanceAndProxyAllowanceLazyStore; private static _throwValidationError( @@ -43,6 +47,11 @@ export class ExchangeTransferSimulator { const errMsg = ERR_MSG_MAPPING[failureReason][tradeSide][transferType]; throw new Error(errMsg); } + /** + * Instantiate a ExchangeTransferSimulator + * @param store A class that implements AbstractBalanceAndProxyAllowanceLazyStore + * @return an instance of ExchangeTransferSimulator + */ constructor(store: AbstractBalanceAndProxyAllowanceLazyStore) { this._store = store; } diff --git a/packages/order-utils/src/index.ts b/packages/order-utils/src/index.ts index 681fbc904..638126af6 100644 --- a/packages/order-utils/src/index.ts +++ b/packages/order-utils/src/index.ts @@ -1,27 +1,36 @@ export { orderHashUtils } from './order_hash'; -export { - isValidSignatureAsync, - isValidPresignedSignatureAsync, - isValidWalletSignatureAsync, - isValidValidatorSignatureAsync, - isValidECSignature, - ecSignOrderHashAsync, - addSignedMessagePrefix, - parseECSignature, -} from './signature_utils'; -export { orderFactory } from './order_factory'; -export { constants } from './constants'; -export { crypto } from './crypto'; +export { signatureUtils } from './signature_utils'; export { generatePseudoRandomSalt } from './salt'; -export { CreateOrderOpts, OrderError, EIP712Parameter, EIP712Schema, EIP712Types } from './types'; +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'; export { AbstractOrderFilledCancelledFetcher } from './abstract/abstract_order_filled_cancelled_fetcher'; -export { BalanceAndProxyAllowanceLazyStore } from './store/balance_and_proxy_allowance_lazy_store'; -export { OrderFilledCancelledLazyStore } from './store/order_filled_cancelled_lazy_store'; -export { RemainingFillableCalculator } from './remaining_fillable_calculator'; -export { OrderStateUtils } from './order_state_utils'; -export { assetDataUtils } from './asset_data_utils'; -export { EIP712Utils } from './eip712_utils'; + export { OrderValidationUtils } from './order_validation_utils'; export { ExchangeTransferSimulator } from './exchange_transfer_simulator'; -export { marketUtils } from './market_utils'; +export { BalanceAndProxyAllowanceLazyStore } from './store/balance_and_proxy_allowance_lazy_store'; +export { OrderFilledCancelledLazyStore } from './store/order_filled_cancelled_lazy_store'; + +export { Provider } from 'ethereum-types'; +export { + SignedOrder, + Order, + OrderRelevantState, + OrderState, + ECSignature, + ERC20AssetData, + ERC721AssetData, + AssetProxyId, +} from '@0xproject/types'; +export { + CreateOrderOpts, + OrderError, + EIP712Parameter, + EIP712Schema, + EIP712Types, + TradeSide, + TransferType, +} from './types'; diff --git a/packages/order-utils/src/monorepo_scripts/postpublish.ts b/packages/order-utils/src/monorepo_scripts/postpublish.ts deleted file mode 100644 index dcb99d0f7..000000000 --- a/packages/order-utils/src/monorepo_scripts/postpublish.ts +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index e732ac8eb..000000000 --- a/packages/order-utils/src/monorepo_scripts/stage_docs.ts +++ /dev/null @@ -1,8 +0,0 @@ -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 index 4a6f3924b..46a69ae4d 100644 --- a/packages/order-utils/src/order_factory.ts +++ b/packages/order-utils/src/order_factory.ts @@ -6,7 +6,7 @@ import * as _ from 'lodash'; import { constants } from './constants'; import { orderHashUtils } from './order_hash'; import { generatePseudoRandomSalt } from './salt'; -import { ecSignOrderHashAsync } from './signature_utils'; +import { signatureUtils } from './signature_utils'; import { CreateOrderOpts } from './types'; export const orderFactory = { @@ -58,7 +58,12 @@ export const orderFactory = { createOrderOpts, ); const orderHash = orderHashUtils.getOrderHashHex(order); - const signature = await ecSignOrderHashAsync(provider, orderHash, makerAddress, SignerType.Default); + const signature = await signatureUtils.ecSignOrderHashAsync( + provider, + orderHash, + makerAddress, + SignerType.Default, + ); const signedOrder: SignedOrder = _.assign(order, { signature }); return signedOrder; }, diff --git a/packages/order-utils/src/order_hash.ts b/packages/order-utils/src/order_hash.ts index 54c500653..8e98f8767 100644 --- a/packages/order-utils/src/order_hash.ts +++ b/packages/order-utils/src/order_hash.ts @@ -3,7 +3,7 @@ import { Order, SignedOrder } from '@0xproject/types'; import * as _ from 'lodash'; import { assert } from './assert'; -import { EIP712Utils } from './eip712_utils'; +import { eip712Utils } from './eip712_utils'; import { EIP712Schema, EIP712Types } from './types'; const INVALID_TAKER_FORMAT = 'instance.takerAddress is not of a type(s) string'; @@ -69,11 +69,11 @@ export const orderHashUtils = { * @return The resulting orderHash from hashing the supplied order as a Buffer */ getOrderHashBuffer(order: SignedOrder | Order): Buffer { - const orderParamsHashBuff = EIP712Utils.structHash(EIP712_ORDER_SCHEMA, order); - const orderHashBuff = EIP712Utils.createEIP712Message(orderParamsHashBuff, order.exchangeAddress); + const orderParamsHashBuff = eip712Utils.structHash(EIP712_ORDER_SCHEMA, order); + const orderHashBuff = eip712Utils.createEIP712Message(orderParamsHashBuff, order.exchangeAddress); return orderHashBuff; }, _getOrderSchemaBuffer(): Buffer { - return EIP712Utils.compileSchema(EIP712_ORDER_SCHEMA); + return eip712Utils.compileSchema(EIP712_ORDER_SCHEMA); }, }; diff --git a/packages/order-utils/src/order_state_utils.ts b/packages/order-utils/src/order_state_utils.ts index 189bf4180..cb08c5ae2 100644 --- a/packages/order-utils/src/order_state_utils.ts +++ b/packages/order-utils/src/order_state_utils.ts @@ -91,6 +91,14 @@ export class OrderStateUtils { throw new Error(ExchangeContractErrs.OrderFillRoundingError); } } + /** + * Instantiate OrderStateUtils + * @param balanceAndProxyAllowanceFetcher A class that is capable of fetching balances + * and proxyAllowances for Ethereum addresses. It must implement AbstractBalanceAndProxyAllowanceFetcher + * @param orderFilledCancelledFetcher A class that is capable of fetching whether an order + * is cancelled and how much of it has been filled. It must implement AbstractOrderFilledCancelledFetcher + * @return Instance of OrderStateUtils + */ constructor( balanceAndProxyAllowanceFetcher: AbstractBalanceAndProxyAllowanceFetcher, orderFilledCancelledFetcher: AbstractOrderFilledCancelledFetcher, @@ -98,6 +106,14 @@ export class OrderStateUtils { this._balanceAndProxyAllowanceFetcher = balanceAndProxyAllowanceFetcher; this._orderFilledCancelledFetcher = orderFilledCancelledFetcher; } + /** + * Get the orderState for an "open" order (i.e where takerAddress=NULL_ADDRESS) + * This method will only check the maker's balance/allowance to calculate the + * OrderState. + * @param signedOrder The order of interest + * @return State relevant to the signedOrder, as well as whether the signedOrder is "valid". + * Validity is defined as a non-zero amount of the order can still be filled. + */ public async getOpenOrderStateAsync(signedOrder: SignedOrder): Promise<OrderState> { const orderRelevantState = await this.getOpenOrderRelevantStateAsync(signedOrder); const orderHash = orderHashUtils.getOrderHashHex(signedOrder); @@ -127,6 +143,11 @@ export class OrderStateUtils { return orderState; } } + /** + * Get state relevant to an order (i.e makerBalance, makerAllowance, filledTakerAssetAmount, etc... + * @param signedOrder Order of interest + * @return An instance of OrderRelevantState + */ public async getOpenOrderRelevantStateAsync(signedOrder: SignedOrder): Promise<OrderRelevantState> { const isMaker = true; const sidedOrderRelevantState = await this._getSidedOrderRelevantStateAsync( @@ -149,6 +170,12 @@ export class OrderStateUtils { }; return orderRelevantState; } + /** + * Get the max amount of the supplied order's takerAmount that could still be filled + * @param signedOrder Order of interest + * @param takerAddress Hypothetical taker of the order + * @return fillableTakerAssetAmount + */ public async getMaxFillableTakerAssetAmountAsync( signedOrder: SignedOrder, takerAddress: string, @@ -181,32 +208,6 @@ export class OrderStateUtils { return fillableTakerAssetAmount; } - public async getMaxFillableTakerAssetAmountForFailingOrderAsync( - signedOrder: SignedOrder, - takerAddress: string, - ): Promise<BigNumber> { - // Get min of taker balance & allowance - const takerAssetBalanceOfTaker = await this._balanceAndProxyAllowanceFetcher.getBalanceAsync( - signedOrder.takerAssetData, - takerAddress, - ); - const takerAssetAllowanceOfTaker = await this._balanceAndProxyAllowanceFetcher.getProxyAllowanceAsync( - signedOrder.takerAssetData, - takerAddress, - ); - const minTakerAssetAmount = BigNumber.min([takerAssetBalanceOfTaker, takerAssetAllowanceOfTaker]); - - // get remainingFillAmount - const orderHash = orderHashUtils.getOrderHashHex(signedOrder); - const filledTakerAssetAmount = await this._orderFilledCancelledFetcher.getFilledTakerAmountAsync(orderHash); - const remainingFillTakerAssetAmount = signedOrder.takerAssetAmount.minus(filledTakerAssetAmount); - - if (minTakerAssetAmount.gte(remainingFillTakerAssetAmount)) { - return remainingFillTakerAssetAmount; - } else { - return minTakerAssetAmount; - } - } private async _getSidedOrderRelevantStateAsync( isMakerSide: boolean, signedOrder: SignedOrder, diff --git a/packages/order-utils/src/order_validation_utils.ts b/packages/order-utils/src/order_validation_utils.ts index 67d747081..972e6f6d6 100644 --- a/packages/order-utils/src/order_validation_utils.ts +++ b/packages/order-utils/src/order_validation_utils.ts @@ -9,11 +9,21 @@ import { AbstractOrderFilledCancelledFetcher } from './abstract/abstract_order_f import { constants } from './constants'; import { ExchangeTransferSimulator } from './exchange_transfer_simulator'; import { orderHashUtils } from './order_hash'; -import { isValidSignatureAsync } from './signature_utils'; +import { signatureUtils } from './signature_utils'; import { utils } from './utils'; +/** + * A utility class for validating orders + */ export class OrderValidationUtils { private readonly _orderFilledCancelledFetcher: AbstractOrderFilledCancelledFetcher; + /** + * A Typescript implementation mirroring the implementation of isRoundingError in the + * Exchange smart contract + * @param numerator Numerator value. When used to check an order, pass in `takerAssetFilledAmount` + * @param denominator Denominator value. When used to check an order, pass in `order.takerAssetAmount` + * @param target Target value. When used to check an order, pass in `order.makerAssetAmount` + */ public static isRoundingError(numerator: BigNumber, denominator: BigNumber, target: BigNumber): boolean { // Solidity's mulmod() in JS // Source: https://solidity.readthedocs.io/en/latest/units-and-global-variables.html#mathematical-and-cryptographic-functions @@ -31,6 +41,15 @@ export class OrderValidationUtils { const isError = errPercentageTimes1000000.gt(1000); return isError; } + /** + * Validate that the maker & taker have sufficient balances/allowances + * to fill the supplied order to the fillTakerAssetAmount amount + * @param exchangeTradeEmulator ExchangeTradeEmulator to use + * @param signedOrder SignedOrder to test + * @param fillTakerAssetAmount Amount of takerAsset to fill the signedOrder + * @param senderAddress Sender of the fillOrder tx + * @param zrxAssetData AssetData for the ZRX token + */ public static async validateFillOrderBalancesAllowancesThrowIfInvalidAsync( exchangeTradeEmulator: ExchangeTransferSimulator, signedOrder: SignedOrder, @@ -104,9 +123,22 @@ export class OrderValidationUtils { throw new Error(RevertReason.OrderUnfillable); } } + /** + * Instantiate OrderValidationUtils + * @param orderFilledCancelledFetcher A module that implements the AbstractOrderFilledCancelledFetcher + * @return An instance of OrderValidationUtils + */ constructor(orderFilledCancelledFetcher: AbstractOrderFilledCancelledFetcher) { this._orderFilledCancelledFetcher = orderFilledCancelledFetcher; } + /** + * Validate if the supplied order is fillable, and throw if it isn't + * @param exchangeTradeEmulator ExchangeTradeEmulator instance + * @param signedOrder SignedOrder of interest + * @param zrxAssetData ZRX assetData + * @param expectedFillTakerTokenAmount If supplied, this call will make sure this amount is fillable. + * If it isn't supplied, we check if the order is fillable for a non-zero amount + */ public async validateOrderFillableOrThrowAsync( exchangeTradeEmulator: ExchangeTransferSimulator, signedOrder: SignedOrder, @@ -132,6 +164,15 @@ export class OrderValidationUtils { zrxAssetData, ); } + /** + * Validate a call to FillOrder and throw if it wouldn't succeed + * @param exchangeTradeEmulator ExchangeTradeEmulator to use + * @param provider Web3 provider to use for JSON RPC requests + * @param signedOrder SignedOrder of interest + * @param fillTakerAssetAmount Amount we'd like to fill the order for + * @param takerAddress The taker of the order + * @param zrxAssetData ZRX asset data + */ public async validateFillOrderThrowIfInvalidAsync( exchangeTradeEmulator: ExchangeTransferSimulator, provider: Provider, @@ -147,7 +188,7 @@ export class OrderValidationUtils { throw new Error(RevertReason.InvalidTakerAmount); } const orderHash = orderHashUtils.getOrderHashHex(signedOrder); - const isValid = await isValidSignatureAsync( + const isValid = await signatureUtils.isValidSignatureAsync( provider, orderHash, signedOrder.signature, @@ -187,6 +228,15 @@ export class OrderValidationUtils { } return filledTakerTokenAmount; } + /** + * Validate a call to fillOrKillOrder and throw if it would fail + * @param exchangeTradeEmulator ExchangeTradeEmulator to use + * @param provider Web3 provider to use for JSON RPC requests + * @param signedOrder SignedOrder of interest + * @param fillTakerAssetAmount Amount we'd like to fill the order for + * @param takerAddress The taker of the order + * @param zrxAssetData ZRX asset data + */ public async validateFillOrKillOrderThrowIfInvalidAsync( exchangeTradeEmulator: ExchangeTransferSimulator, provider: Provider, diff --git a/packages/order-utils/src/signature_utils.ts b/packages/order-utils/src/signature_utils.ts index 870aef2ed..d466bb00d 100644 --- a/packages/order-utils/src/signature_utils.ts +++ b/packages/order-utils/src/signature_utils.ts @@ -13,329 +13,353 @@ import { IWalletContract } from './generated_contract_wrappers/i_wallet'; import { OrderError } from './types'; import { utils } from './utils'; -/** - * Verifies that the provided signature is valid according to the 0x Protocol smart contracts - * @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] - * @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 async function isValidSignatureAsync( - provider: Provider, - data: string, - signature: string, - signerAddress: string, -): Promise<boolean> { - assert.isWeb3Provider('provider', provider); - assert.isHexString('data', data); - assert.isHexString('signature', signature); - assert.isETHAddressHex('signerAddress', signerAddress); - const signatureTypeIndexIfExists = utils.getSignatureTypeIndexIfExists(signature); - if (_.isUndefined(signatureTypeIndexIfExists)) { - throw new Error(`Unrecognized signatureType in signature: ${signature}`); - } - - switch (signatureTypeIndexIfExists) { - case SignatureType.Illegal: - case SignatureType.Invalid: - return false; - - case SignatureType.EIP712: { - const ecSignature = parseECSignature(signature); - return isValidECSignature(data, ecSignature, signerAddress); +export const signatureUtils = { + /** + * Verifies that the provided signature is valid according to the 0x Protocol smart contracts + * @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] + * @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. + */ + async isValidSignatureAsync( + provider: Provider, + data: string, + signature: string, + signerAddress: string, + ): Promise<boolean> { + assert.isWeb3Provider('provider', provider); + assert.isHexString('data', data); + assert.isHexString('signature', signature); + assert.isETHAddressHex('signerAddress', signerAddress); + const signatureTypeIndexIfExists = utils.getSignatureTypeIndexIfExists(signature); + if (_.isUndefined(signatureTypeIndexIfExists)) { + throw new Error(`Unrecognized signatureType in signature: ${signature}`); } - case SignatureType.EthSign: { - const ecSignature = parseECSignature(signature); - const prefixedMessageHex = addSignedMessagePrefix(data, SignerType.Default); - return isValidECSignature(prefixedMessageHex, ecSignature, signerAddress); - } + switch (signatureTypeIndexIfExists) { + case SignatureType.Illegal: + case SignatureType.Invalid: + return false; - case SignatureType.Caller: - // HACK: We currently do not "validate" the caller signature type. - // It can only be validated during Exchange contract execution. - throw new Error('Caller signature type cannot be validated off-chain'); + case SignatureType.EIP712: { + const ecSignature = signatureUtils.parseECSignature(signature); + return signatureUtils.isValidECSignature(data, ecSignature, signerAddress); + } - case SignatureType.Wallet: { - const isValid = await isValidWalletSignatureAsync(provider, data, signature, signerAddress); - return isValid; - } + case SignatureType.EthSign: { + const ecSignature = signatureUtils.parseECSignature(signature); + const prefixedMessageHex = signatureUtils.addSignedMessagePrefix(data, SignerType.EthSign); + return signatureUtils.isValidECSignature(prefixedMessageHex, ecSignature, signerAddress); + } - case SignatureType.Validator: { - const isValid = await isValidValidatorSignatureAsync(provider, data, signature, signerAddress); - return isValid; - } + case SignatureType.Caller: + // HACK: We currently do not "validate" the caller signature type. + // It can only be validated during Exchange contract execution. + throw new Error('Caller signature type cannot be validated off-chain'); - case SignatureType.PreSigned: { - return isValidPresignedSignatureAsync(provider, data, signerAddress); - } + case SignatureType.Wallet: { + const isValid = await signatureUtils.isValidWalletSignatureAsync( + provider, + data, + signature, + signerAddress, + ); + return isValid; + } - case SignatureType.Trezor: { - const prefixedMessageHex = addSignedMessagePrefix(data, SignerType.Trezor); - const ecSignature = parseECSignature(signature); - return isValidECSignature(prefixedMessageHex, ecSignature, signerAddress); - } + case SignatureType.Validator: { + const isValid = await signatureUtils.isValidValidatorSignatureAsync( + provider, + data, + signature, + signerAddress, + ); + return isValid; + } - default: - throw new Error(`Unhandled SignatureType: ${signatureTypeIndexIfExists}`); - } -} + case SignatureType.PreSigned: { + return signatureUtils.isValidPresignedSignatureAsync(provider, data, signerAddress); + } -/** - * Verifies that the provided presigned signature is valid according to the 0x Protocol smart contracts - * @param data The hex encoded data signed by the supplied signature. - * @param signature A hex encoded presigned 0x Protocol signature made up of: [SignatureType.Presigned] - * @param signerAddress The hex encoded address that signed the data, producing the supplied signature. - * @return Whether the data was preSigned by the supplied signerAddress. - */ -export async function isValidPresignedSignatureAsync( - provider: Provider, - data: string, - signerAddress: string, -): Promise<boolean> { - assert.isWeb3Provider('provider', provider); - assert.isHexString('data', data); - assert.isETHAddressHex('signerAddress', signerAddress); - const exchangeContract = new ExchangeContract(artifacts.Exchange.compilerOutput.abi, signerAddress, provider); - const isValid = await exchangeContract.preSigned.callAsync(data, signerAddress); - return isValid; -} - -/** - * Verifies that the provided wallet signature is valid according to the 0x Protocol smart contracts - * @param data The hex encoded data signed by the supplied signature. - * @param signature A hex encoded presigned 0x Protocol signature made up of: [SignatureType.Presigned] - * @param signerAddress The hex encoded address that signed the data, producing the supplied signature. - * @return Whether the data was preSigned by the supplied signerAddress. - */ -export async function isValidWalletSignatureAsync( - provider: Provider, - data: string, - signature: string, - signerAddress: string, -): Promise<boolean> { - assert.isWeb3Provider('provider', provider); - assert.isHexString('data', data); - assert.isHexString('signature', signature); - assert.isETHAddressHex('signerAddress', signerAddress); - // tslint:disable-next-line:custom-no-magic-numbers - const signatureWithoutType = signature.slice(-2); - const walletContract = new IWalletContract(artifacts.IWallet.compilerOutput.abi, signerAddress, provider); - const isValid = await walletContract.isValidSignature.callAsync(data, signatureWithoutType); - return isValid; -} - -/** - * Verifies that the provided validator signature is valid according to the 0x Protocol smart contracts - * @param data The hex encoded data signed by the supplied signature. - * @param signature A hex encoded presigned 0x Protocol signature made up of: [SignatureType.Presigned] - * @param signerAddress The hex encoded address that signed the data, producing the supplied signature. - * @return Whether the data was preSigned by the supplied signerAddress. - */ -export async function isValidValidatorSignatureAsync( - provider: Provider, - data: string, - signature: string, - signerAddress: string, -): Promise<boolean> { - assert.isWeb3Provider('provider', provider); - assert.isHexString('data', data); - assert.isHexString('signature', signature); - assert.isETHAddressHex('signerAddress', signerAddress); - const validatorSignature = parseValidatorSignature(signature); - const exchangeContract = new ExchangeContract(artifacts.Exchange.compilerOutput.abi, signerAddress, provider); - const isValidatorApproved = await exchangeContract.allowedValidators.callAsync( - signerAddress, - validatorSignature.validatorAddress, - ); - if (!isValidatorApproved) { - throw new Error(`Validator ${validatorSignature.validatorAddress} was not pre-approved by ${signerAddress}.`); - } + case SignatureType.Trezor: { + const prefixedMessageHex = signatureUtils.addSignedMessagePrefix(data, SignerType.Trezor); + const ecSignature = signatureUtils.parseECSignature(signature); + return signatureUtils.isValidECSignature(prefixedMessageHex, ecSignature, signerAddress); + } - const validatorContract = new IValidatorContract(artifacts.IValidator.compilerOutput.abi, signerAddress, provider); - const isValid = await validatorContract.isValidSignature.callAsync( - data, - signerAddress, - validatorSignature.signature, - ); - return isValid; -} - -/** - * Checks if the supplied elliptic curve signature corresponds to signing `data` with - * the private key corresponding to `signerAddress` - * @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 ECSignature is valid. - */ -export function isValidECSignature(data: string, signature: ECSignature, signerAddress: string): boolean { - assert.isHexString('data', data); - assert.doesConformToSchema('signature', signature, schemas.ecSignatureSchema); - assert.isETHAddressHex('signerAddress', signerAddress); - - const msgHashBuff = ethUtil.toBuffer(data); - try { - const pubKey = ethUtil.ecrecover( - msgHashBuff, - signature.v, - ethUtil.toBuffer(signature.r), - ethUtil.toBuffer(signature.s), + default: + throw new Error(`Unhandled SignatureType: ${signatureTypeIndexIfExists}`); + } + }, + /** + * Verifies that the provided presigned 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 signerAddress The hex encoded address that signed the data, producing the supplied signature. + * @return Whether the data was preSigned by the supplied signerAddress + */ + async isValidPresignedSignatureAsync(provider: Provider, data: string, signerAddress: string): Promise<boolean> { + assert.isWeb3Provider('provider', provider); + assert.isHexString('data', data); + assert.isETHAddressHex('signerAddress', signerAddress); + const exchangeContract = new ExchangeContract(artifacts.Exchange.compilerOutput.abi, signerAddress, provider); + const isValid = await exchangeContract.preSigned.callAsync(data, signerAddress); + return isValid; + }, + /** + * Verifies that the provided wallet 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 presigned 0x Protocol signature made up of: [SignatureType.Presigned] + * @param signerAddress The hex encoded address that signed the data, producing the supplied signature. + * @return Whether the data was preSigned by the supplied signerAddress. + */ + async isValidWalletSignatureAsync( + provider: Provider, + data: string, + signature: string, + signerAddress: string, + ): Promise<boolean> { + assert.isWeb3Provider('provider', provider); + assert.isHexString('data', data); + assert.isHexString('signature', signature); + assert.isETHAddressHex('signerAddress', signerAddress); + // tslint:disable-next-line:custom-no-magic-numbers + const signatureWithoutType = signature.slice(-2); + const walletContract = new IWalletContract(artifacts.IWallet.compilerOutput.abi, signerAddress, provider); + const isValid = await walletContract.isValidSignature.callAsync(data, signatureWithoutType); + return isValid; + }, + /** + * Verifies that the provided validator 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 presigned 0x Protocol signature made up of: [SignatureType.Presigned] + * @param signerAddress The hex encoded address that signed the data, producing the supplied signature. + * @return Whether the data was preSigned by the supplied signerAddress. + */ + async isValidValidatorSignatureAsync( + provider: Provider, + data: string, + signature: string, + signerAddress: string, + ): Promise<boolean> { + assert.isWeb3Provider('provider', provider); + assert.isHexString('data', data); + assert.isHexString('signature', signature); + assert.isETHAddressHex('signerAddress', signerAddress); + const validatorSignature = parseValidatorSignature(signature); + const exchangeContract = new ExchangeContract(artifacts.Exchange.compilerOutput.abi, signerAddress, provider); + const isValidatorApproved = await exchangeContract.allowedValidators.callAsync( + signerAddress, + validatorSignature.validatorAddress, ); - const retrievedAddress = ethUtil.bufferToHex(ethUtil.pubToAddress(pubKey)); - return retrievedAddress === signerAddress; - } catch (err) { - return false; - } -} - -/** - * 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 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 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. - */ -export async function ecSignOrderHashAsync( - provider: Provider, - orderHash: string, - signerAddress: string, - signerType: SignerType, -): Promise<string> { - assert.isWeb3Provider('provider', provider); - assert.isHexString('orderHash', orderHash); - assert.isETHAddressHex('signerAddress', signerAddress); - const web3Wrapper = new Web3Wrapper(provider); - await assert.isSenderAddressAsync('signerAddress', signerAddress, web3Wrapper); - const normalizedSignerAddress = signerAddress.toLowerCase(); + if (!isValidatorApproved) { + throw new Error( + `Validator ${validatorSignature.validatorAddress} was not pre-approved by ${signerAddress}.`, + ); + } - let msgHashHex = orderHash; - const prefixedMsgHashHex = 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); + const validatorContract = new IValidatorContract( + artifacts.IValidator.compilerOutput.abi, + signerAddress, + provider, + ); + const isValid = await validatorContract.isValidSignature.callAsync( + data, + signerAddress, + validatorSignature.signature, + ); + return isValid; + }, + /** + * Checks if the supplied elliptic curve signature corresponds to signing `data` with + * the private key corresponding to `signerAddress` + * @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 ECSignature is valid. + */ + isValidECSignature(data: string, signature: ECSignature, signerAddress: string): boolean { + assert.isHexString('data', data); + assert.doesConformToSchema('signature', signature, schemas.ecSignatureSchema); + assert.isETHAddressHex('signerAddress', signerAddress); - // 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. - // 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 = isValidECSignature(prefixedMsgHashHex, ecSignatureRSV, normalizedSignerAddress); - if (isValidRSVSignature) { - const convertedSignatureHex = convertECSignatureToSignatureHex(ecSignatureRSV, signerType); - return convertedSignatureHex; + const msgHashBuff = ethUtil.toBuffer(data); + 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; } - } - const ecSignatureVRS = parseSignatureHexAsVRS(signature); - if (_.includes(validVParamValues, ecSignatureVRS.v)) { - const isValidVRSSignature = isValidECSignature(prefixedMsgHashHex, ecSignatureVRS, normalizedSignerAddress); - if (isValidVRSSignature) { - const convertedSignatureHex = convertECSignatureToSignatureHex(ecSignatureVRS, signerType); - return convertedSignatureHex; + }, + /** + * 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 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 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, + signerType: SignerType, + ): Promise<string> { + assert.isWeb3Provider('provider', provider); + assert.isHexString('orderHash', orderHash); + assert.isETHAddressHex('signerAddress', signerAddress); + const web3Wrapper = new Web3Wrapper(provider); + await assert.isSenderAddressAsync('signerAddress', signerAddress, web3Wrapper); + const normalizedSignerAddress = signerAddress.toLowerCase(); + + let msgHashHex = orderHash; + 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); - 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 - */ -export function 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; + // 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. + // 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; + } } - case SignerType.Trezor: { - signatureType = SignatureType.Trezor; - break; + const ecSignatureVRS = parseSignatureHexAsVRS(signature); + if (_.includes(validVParamValues, ecSignatureVRS.v)) { + const isValidVRSSignature = signatureUtils.isValidECSignature( + prefixedMsgHashHex, + ecSignatureVRS, + normalizedSignerAddress, + ); + if (isValidVRSSignature) { + const convertedSignatureHex = signatureUtils.convertECSignatureToSignatureHex( + ecSignatureVRS, + signerType, + ); + return convertedSignatureHex; + } } - default: - throw new Error(`Unrecognized SignerType: ${signerType}`); - } - const signatureWithType = 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 - */ -export function 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 signerType The type of message prefix to add for a given SignerType. Different signers expect - * specific message prefixes. - * @return Prefixed message - */ -export function addSignedMessagePrefix(message: string, signerType: SignerType = SignerType.Default): string { - assert.isString('message', message); - 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; + + 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}`); } - case SignerType.Trezor: { - const msgBuff = ethUtil.toBuffer(message); - const prefixedMsgBuff = hashTrezorPersonalMessage(msgBuff); - const prefixedMsgHex = ethUtil.bufferToHex(prefixedMsgBuff); - return prefixedMsgHex; + 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 signerType The type of message prefix to add for a given SignerType. Different signers expect + * specific message prefixes. + * @return Prefixed message + */ + addSignedMessagePrefix(message: string, signerType: SignerType = SignerType.Default): string { + assert.isString('message', message); + 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 SignerType.Trezor: { + const msgBuff = ethUtil.toBuffer(message); + const prefixedMsgBuff = hashTrezorPersonalMessage(msgBuff); + const prefixedMsgHex = ethUtil.bufferToHex(prefixedMsgBuff); + return prefixedMsgHex; + } + default: + throw new Error(`Unrecognized SignerType: ${signerType}`); } - default: - throw new Error(`Unrecognized SignerType: ${signerType}`); - } -} - -/** - * Parse a 0x protocol hex-encoded signature string into it's ECSignature components - * @param signature A hex encoded ecSignature 0x Protocol signature - * @return An ECSignature object with r,s,v parameters - */ -export function parseECSignature(signature: string): ECSignature { - assert.isHexString('signature', signature); - const ecSignatureTypes = [SignatureType.EthSign, SignatureType.EIP712, SignatureType.Trezor]; - assert.isOneOfExpectedSignatureTypes(signature, ecSignatureTypes); + }, + /** + * Parse a 0x protocol hex-encoded signature string into it's ECSignature components + * @param signature A hex encoded ecSignature 0x Protocol signature + * @return An ECSignature object with r,s,v parameters + */ + parseECSignature(signature: string): ECSignature { + assert.isHexString('signature', signature); + const ecSignatureTypes = [SignatureType.EthSign, SignatureType.EIP712, SignatureType.Trezor]; + assert.isOneOfExpectedSignatureTypes(signature, ecSignatureTypes); - // tslint:disable-next-line:custom-no-magic-numbers - const vrsHex = signature.slice(0, -2); - const ecSignature = parseSignatureHexAsVRS(vrsHex); + // tslint:disable-next-line:custom-no-magic-numbers + const vrsHex = signature.slice(0, -2); + const ecSignature = parseSignatureHexAsVRS(vrsHex); - return ecSignature; -} + return ecSignature; + }, +}; function hashTrezorPersonalMessage(message: Buffer): Buffer { const prefix = ethUtil.toBuffer('\x19Ethereum Signed Message:\n' + String.fromCharCode(message.byteLength)); diff --git a/packages/order-utils/src/store/balance_and_proxy_allowance_lazy_store.ts b/packages/order-utils/src/store/balance_and_proxy_allowance_lazy_store.ts index 5a2c1d7ff..8a65178b0 100644 --- a/packages/order-utils/src/store/balance_and_proxy_allowance_lazy_store.ts +++ b/packages/order-utils/src/store/balance_and_proxy_allowance_lazy_store.ts @@ -21,11 +21,21 @@ export class BalanceAndProxyAllowanceLazyStore implements AbstractBalanceAndProx [userAddress: string]: BigNumber; }; }; + /** + * Instantiates a BalanceAndProxyAllowanceLazyStore + * @param balanceAndProxyAllowanceFetcher Class the implements the AbstractBalanceAndProxyAllowanceFetcher + * @return Instance of BalanceAndProxyAllowanceLazyStore + */ constructor(balanceAndProxyAllowanceFetcher: AbstractBalanceAndProxyAllowanceFetcher) { this._balanceAndProxyAllowanceFetcher = balanceAndProxyAllowanceFetcher; this._balance = {}; this._proxyAllowance = {}; } + /** + * Get a users balance of an asset + * @param assetData AssetData of interest + * @param userAddress Ethereum address of interest + */ public async getBalanceAsync(assetData: string, userAddress: string): Promise<BigNumber> { if (_.isUndefined(this._balance[assetData]) || _.isUndefined(this._balance[assetData][userAddress])) { const balance = await this._balanceAndProxyAllowanceFetcher.getBalanceAsync(assetData, userAddress); @@ -34,12 +44,22 @@ export class BalanceAndProxyAllowanceLazyStore implements AbstractBalanceAndProx const cachedBalance = this._balance[assetData][userAddress]; return cachedBalance; } + /** + * Set the balance of an asset for a user + * @param assetData AssetData of interest + * @param userAddress Ethereum address of interest + */ public setBalance(assetData: string, userAddress: string, balance: BigNumber): void { if (_.isUndefined(this._balance[assetData])) { this._balance[assetData] = {}; } this._balance[assetData][userAddress] = balance; } + /** + * Clear the balance of an asset for a user + * @param assetData AssetData of interest + * @param userAddress Ethereum address of interest + */ public deleteBalance(assetData: string, userAddress: string): void { if (!_.isUndefined(this._balance[assetData])) { delete this._balance[assetData][userAddress]; @@ -48,6 +68,11 @@ export class BalanceAndProxyAllowanceLazyStore implements AbstractBalanceAndProx } } } + /** + * Get the 0x asset proxy allowance + * @param assetData AssetData of interest + * @param userAddress Ethereum address of interest + */ public async getProxyAllowanceAsync(assetData: string, userAddress: string): Promise<BigNumber> { if ( _.isUndefined(this._proxyAllowance[assetData]) || @@ -62,12 +87,22 @@ export class BalanceAndProxyAllowanceLazyStore implements AbstractBalanceAndProx const cachedProxyAllowance = this._proxyAllowance[assetData][userAddress]; return cachedProxyAllowance; } + /** + * Set the 0x asset proxy allowance + * @param assetData AssetData of interest + * @param userAddress Ethereum address of interest + */ public setProxyAllowance(assetData: string, userAddress: string, proxyAllowance: BigNumber): void { if (_.isUndefined(this._proxyAllowance[assetData])) { this._proxyAllowance[assetData] = {}; } this._proxyAllowance[assetData][userAddress] = proxyAllowance; } + /** + * Clear the 0x asset proxy allowance + * @param assetData AssetData of interest + * @param userAddress Ethereum address of interest + */ public deleteProxyAllowance(assetData: string, userAddress: string): void { if (!_.isUndefined(this._proxyAllowance[assetData])) { delete this._proxyAllowance[assetData][userAddress]; @@ -76,6 +111,11 @@ export class BalanceAndProxyAllowanceLazyStore implements AbstractBalanceAndProx } } } + /** + * Clear all ERC721 0x proxy allowances a user has on all items of a specific ERC721 contract + * @param tokenAddress ERc721 token address + * @param userAddress Owner Ethereum address + */ public deleteAllERC721ProxyAllowance(tokenAddress: string, userAddress: string): void { for (const assetData in this._proxyAllowance) { if (this._proxyAllowance.hasOwnProperty(assetData)) { @@ -90,6 +130,9 @@ export class BalanceAndProxyAllowanceLazyStore implements AbstractBalanceAndProx } } } + /** + * Delete all balances & allowances + */ public deleteAll(): void { this._balance = {}; this._proxyAllowance = {}; diff --git a/packages/order-utils/src/store/order_filled_cancelled_lazy_store.ts b/packages/order-utils/src/store/order_filled_cancelled_lazy_store.ts index 336c6d0ba..6155c2064 100644 --- a/packages/order-utils/src/store/order_filled_cancelled_lazy_store.ts +++ b/packages/order-utils/src/store/order_filled_cancelled_lazy_store.ts @@ -15,11 +15,21 @@ export class OrderFilledCancelledLazyStore implements AbstractOrderFilledCancell private _isCancelled: { [orderHash: string]: boolean; }; + /** + * Instantiate a OrderFilledCancelledLazyStore + * @param orderFilledCancelledFetcher Class instance that implements the AbstractOrderFilledCancelledFetcher + * @returns An instance of OrderFilledCancelledLazyStore + */ constructor(orderFilledCancelledFetcher: AbstractOrderFilledCancelledFetcher) { this._orderFilledCancelledFetcher = orderFilledCancelledFetcher; this._filledTakerAmount = {}; this._isCancelled = {}; } + /** + * Get the filledTakerAssetAmount of an order + * @param orderHash OrderHash from order of interest + * @return filledTakerAssetAmount + */ public async getFilledTakerAmountAsync(orderHash: string): Promise<BigNumber> { if (_.isUndefined(this._filledTakerAmount[orderHash])) { const filledTakerAmount = await this._orderFilledCancelledFetcher.getFilledTakerAmountAsync(orderHash); @@ -28,12 +38,26 @@ export class OrderFilledCancelledLazyStore implements AbstractOrderFilledCancell const cachedFilledTakerAmount = this._filledTakerAmount[orderHash]; return cachedFilledTakerAmount; } + /** + * Set the filledTakerAssetAmount of an order + * @param orderHash OrderHash from order of interest + * @param filledTakerAmount Desired filledTakerAssetAmount + */ public setFilledTakerAmount(orderHash: string, filledTakerAmount: BigNumber): void { this._filledTakerAmount[orderHash] = filledTakerAmount; } + /** + * Clear the filledTakerAssetAmount of an order + * @param orderHash OrderHash from order of interest + */ public deleteFilledTakerAmount(orderHash: string): void { delete this._filledTakerAmount[orderHash]; } + /** + * Check if an order has been cancelled + * @param orderHash OrderHash from order of interest + * @return Whether the order has been cancelled + */ public async getIsCancelledAsync(orderHash: string): Promise<boolean> { if (_.isUndefined(this._isCancelled[orderHash])) { const isCancelled = await this._orderFilledCancelledFetcher.isOrderCancelledAsync(orderHash); @@ -42,22 +66,43 @@ export class OrderFilledCancelledLazyStore implements AbstractOrderFilledCancell const cachedIsCancelled = this._isCancelled[orderHash]; // tslint:disable-line:boolean-naming return cachedIsCancelled; } + /** + * Set whether an order has been cancelled or not + * @param orderHash OrderHash from order of interest + * @param isCancelled Whether this order should be cancelled or not + */ public setIsCancelled(orderHash: string, isCancelled: boolean): void { this._isCancelled[orderHash] = isCancelled; } + /** + * Clear whether the order has been cancelled if already set + * @param orderHash OrderHash from order of interest + */ public deleteIsCancelled(orderHash: string): void { delete this._isCancelled[orderHash]; } + /** + * Clear all filled/cancelled state + */ public deleteAll(): void { this.deleteAllFilled(); this.deleteAllIsCancelled(); } + /** + * Clear all cancelled state + */ public deleteAllIsCancelled(): void { this._isCancelled = {}; } + /** + * Clear all filled state + */ public deleteAllFilled(): void { this._filledTakerAmount = {}; } + /** + * Get the ZRX assetData + */ public getZRXAssetData(): string { const zrxAssetData = this._orderFilledCancelledFetcher.getZRXAssetData(); return zrxAssetData; diff --git a/packages/order-utils/test/order_hash_test.ts b/packages/order-utils/test/order_hash_test.ts index 0a6be83d0..3fdbbad21 100644 --- a/packages/order-utils/test/order_hash_test.ts +++ b/packages/order-utils/test/order_hash_test.ts @@ -3,7 +3,9 @@ import { BigNumber } from '@0xproject/utils'; import * as chai from 'chai'; import 'mocha'; -import { constants, orderHashUtils } from '../src'; +import { orderHashUtils } from '../src'; + +import { constants } from '../src/constants'; import { chaiSetup } from './utils/chai_setup'; diff --git a/packages/order-utils/test/signature_utils_test.ts b/packages/order-utils/test/signature_utils_test.ts index a25d2afd6..06ff86515 100644 --- a/packages/order-utils/test/signature_utils_test.ts +++ b/packages/order-utils/test/signature_utils_test.ts @@ -6,8 +6,8 @@ import * as _ from 'lodash'; import 'mocha'; import * as Sinon from 'sinon'; -import { ecSignOrderHashAsync, generatePseudoRandomSalt } from '../src'; -import { convertECSignatureToSignatureHex, isValidECSignature, isValidSignatureAsync } 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'; @@ -24,12 +24,14 @@ describe('Signature utils', () => { it("should return false if the data doesn't pertain to the signature & address", async () => { const bytes32Zeros = '0x0000000000000000000000000000000000000000000000000000000000000000'; - expect(await isValidSignatureAsync(provider, bytes32Zeros, ethSignSignature, address)).to.be.false(); + expect( + 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 () => { const validUnrelatedAddress = '0x8b0292b11a196601ed2ce54b665cafeca0347d42'; expect( - await isValidSignatureAsync(provider, dataHex, ethSignSignature, validUnrelatedAddress), + await signatureUtils.isValidSignatureAsync(provider, dataHex, ethSignSignature, validUnrelatedAddress), ).to.be.false(); }); it("should return false if the signature doesn't pertain to the dataHex & address", async () => { @@ -37,18 +39,27 @@ describe('Signature utils', () => { // tslint:disable-next-line:custom-no-magic-numbers signatureArray[5] = 'C'; // V = 28, instead of 27 const wrongSignature = signatureArray.join(''); - expect(await isValidSignatureAsync(provider, dataHex, wrongSignature, address)).to.be.false(); + expect( + await signatureUtils.isValidSignatureAsync(provider, dataHex, wrongSignature, address), + ).to.be.false(); }); it('should throw if signatureType is invalid', () => { const signatureArray = ethSignSignature.split(''); signatureArray[3] = '9'; // SignatureType w/ index 9 doesn't exist const signatureWithInvalidType = signatureArray.join(''); - expect(isValidSignatureAsync(provider, dataHex, signatureWithInvalidType, address)).to.be.rejected(); + expect( + signatureUtils.isValidSignatureAsync(provider, dataHex, signatureWithInvalidType, address), + ).to.be.rejected(); }); it('should return true for a valid Ecrecover (EthSign) signature', async () => { - const isValidSignatureLocal = await isValidSignatureAsync(provider, dataHex, ethSignSignature, address); + const isValidSignatureLocal = await signatureUtils.isValidSignatureAsync( + provider, + dataHex, + ethSignSignature, + address, + ); expect(isValidSignatureLocal).to.be.true(); }); @@ -57,7 +68,12 @@ describe('Signature utils', () => { address = '0x6ecbe1db9ef729cbe972c83fb886247691fb6beb'; const eip712Signature = '0x1bdde07aac4bf12c12ddbb155919c43eba4146a2cfcf904a862950dbebe332554c6674975603eb5a4eaf8fd7f2e06350267e5b36cda9851a89f8bb49fe2fc9afe202'; - const isValidSignatureLocal = await isValidSignatureAsync(provider, dataHex, eip712Signature, address); + const isValidSignatureLocal = await signatureUtils.isValidSignatureAsync( + provider, + dataHex, + eip712Signature, + address, + ); expect(isValidSignatureLocal).to.be.true(); }); @@ -66,7 +82,12 @@ describe('Signature utils', () => { address = '0x6ecbe1db9ef729cbe972c83fb886247691fb6beb'; const trezorSignature = '0x1ce4760660e6495b5ae6723087bea073b3a99ce98ea81fdf00c240279c010e63d05b87bc34c4d67d4776e8d5aeb023a67484f4eaf0fd353b40893e5101e845cd9908'; - const isValidSignatureLocal = await isValidSignatureAsync(provider, dataHex, trezorSignature, address); + const isValidSignatureLocal = await signatureUtils.isValidSignatureAsync( + provider, + dataHex, + trezorSignature, + address, + ); expect(isValidSignatureLocal).to.be.true(); }); }); @@ -80,18 +101,18 @@ describe('Signature utils', () => { const address = '0x0e5cb767cce09a7f3ca594df118aa519be5e2b5a'; it("should return false if the data doesn't pertain to the signature & address", async () => { - expect(isValidECSignature('0x0', signature, address)).to.be.false(); + expect(signatureUtils.isValidECSignature('0x0', signature, address)).to.be.false(); }); it("should return false if the address doesn't pertain to the signature & data", async () => { const validUnrelatedAddress = '0x8b0292b11a196601ed2ce54b665cafeca0347d42'; - expect(isValidECSignature(data, signature, validUnrelatedAddress)).to.be.false(); + expect(signatureUtils.isValidECSignature(data, signature, validUnrelatedAddress)).to.be.false(); }); it("should return false if the signature doesn't pertain to the data & address", async () => { const wrongSignature = _.assign({}, signature, { v: 28 }); - expect(isValidECSignature(data, wrongSignature, address)).to.be.false(); + expect(signatureUtils.isValidECSignature(data, wrongSignature, address)).to.be.false(); }); it('should return true if the signature does pertain to the data & address', async () => { - const isValidSignatureLocal = isValidECSignature(data, signature, address); + const isValidSignatureLocal = signatureUtils.isValidECSignature(data, signature, address); expect(isValidSignatureLocal).to.be.true(); }); }); @@ -124,7 +145,12 @@ describe('Signature utils', () => { const orderHash = '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0'; const expectedSignature = '0x1b61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc3340349190569279751135161d22529dc25add4f6069af05be04cacbda2ace225403'; - const ecSignature = await ecSignOrderHashAsync(provider, orderHash, makerAddress, SignerType.Default); + const ecSignature = await signatureUtils.ecSignOrderHashAsync( + provider, + orderHash, + makerAddress, + SignerType.Default, + ); expect(ecSignature).to.equal(expectedSignature); }); it('should return the correct Signature for signatureHex concatenated as R + S + V', async () => { @@ -150,7 +176,12 @@ describe('Signature utils', () => { } }, }; - const ecSignature = await ecSignOrderHashAsync(fakeProvider, orderHash, makerAddress, SignerType.Default); + const ecSignature = await signatureUtils.ecSignOrderHashAsync( + fakeProvider, + orderHash, + makerAddress, + SignerType.Default, + ); expect(ecSignature).to.equal(expectedSignature); }); it('should return the correct Signature for signatureHex concatenated as V + R + S', async () => { @@ -173,7 +204,12 @@ describe('Signature utils', () => { }, }; - const ecSignature = await ecSignOrderHashAsync(fakeProvider, orderHash, makerAddress, SignerType.Default); + 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 @@ -202,12 +238,22 @@ describe('Signature utils', () => { }, }; - const ecSignature = await ecSignOrderHashAsync(fakeProvider, orderHash, makerAddress, SignerType.Metamask); + const ecSignature = await signatureUtils.ecSignOrderHashAsync( + fakeProvider, + orderHash, + makerAddress, + SignerType.Metamask, + ); expect(ecSignature).to.equal(expectedSignature); }); it('should return a valid signature', async () => { const orderHash = '0x34decbedc118904df65f379a175bb39ca18209d6ce41d5ed549d54e6e0a95004'; - const ecSignature = await ecSignOrderHashAsync(provider, orderHash, makerAddress, SignerType.Default); + const ecSignature = await signatureUtils.ecSignOrderHashAsync( + provider, + orderHash, + makerAddress, + SignerType.Default, + ); const isValidSignature = await isValidSignatureAsync(provider, orderHash, ecSignature, makerAddress); expect(isValidSignature).to.be.true(); |