Merge branch 'development' into development

This commit is contained in:
Andrew Maurer 2018-10-13 11:09:50 -04:00 committed by GitHub
commit 69e9dbd683
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
139 changed files with 3905 additions and 1403 deletions

View File

@ -23,6 +23,7 @@ jobs:
paths: paths:
- ~/repo - ~/repo
build-website: build-website:
resource_class: medium+
docker: docker:
- image: circleci/node:9 - image: circleci/node:9
working_directory: ~/repo working_directory: ~/repo
@ -161,6 +162,90 @@ jobs:
key: coverage-web3-wrapper-{{ .Environment.CIRCLE_SHA1 }} key: coverage-web3-wrapper-{{ .Environment.CIRCLE_SHA1 }}
paths: paths:
- ~/repo/packages/web3-wrapper/coverage/lcov.info - ~/repo/packages/web3-wrapper/coverage/lcov.info
test-python:
working_directory: ~/repo
docker:
- image: circleci/python
steps:
- checkout
- run: sudo chown -R circleci:circleci /usr/local/bin
- run: sudo chown -R circleci:circleci /usr/local/lib/python3.7/site-packages
- restore_cache:
key: deps9-{{ .Branch }}-{{ .Environment.CIRCLE_SHA1 }}
- run:
command: |
cd python-packages/order_utils
python -m ensurepip
python -m pip install -e .[dev]
- save_cache:
key: deps9-{{ .Branch }}-{{ .Environment.CIRCLE_SHA1 }}
paths:
- "/usr/local/bin"
- "/usr/local/lib/python3.7/site-packages"
- ".eggs"
- ".mypy_cache"
- ".pytest_cache"
- ".tox"
- run:
command: |
cd python-packages/order_utils
coverage run setup.py test
- save_cache:
key: coverage-python-order-utils-{{ .Environment.CIRCLE_SHA1 }}
paths:
- ~/repo/python-packages/order_utils/.coverage
test-rest-python:
working_directory: ~/repo
docker:
- image: circleci/python
steps:
- checkout
- run: sudo chown -R circleci:circleci /usr/local/bin
- run: sudo chown -R circleci:circleci /usr/local/lib/python3.7/site-packages
- restore_cache:
key: deps9-{{ .Branch }}-{{ .Environment.CIRCLE_SHA1 }}
- run:
command: |
cd python-packages/order_utils
python -m ensurepip
python -m pip install -e .[dev]
- save_cache:
key: deps9-{{ .Branch }}-{{ .Environment.CIRCLE_SHA1 }}
paths:
- "/usr/local/bin"
- "/usr/local/lib/python3.7/site-packages"
- ".eggs"
- ".mypy_cache"
- ".pytest_cache"
- ".tox"
- run:
command: |
cd python-packages/order_utils
tox
static-tests-python:
working_directory: ~/repo
docker:
- image: circleci/python
steps:
- checkout
- run: sudo chown -R circleci:circleci /usr/local/bin
- run: sudo chown -R circleci:circleci /usr/local/lib/python3.7/site-packages
- restore_cache:
key: deps9-{{ .Branch }}-{{ .Environment.CIRCLE_SHA1 }}
- run:
command: |
cd python-packages/order_utils
python -m ensurepip
python -m pip install -e .[dev]
- save_cache:
key: deps9-{{ .Branch }}-{{ .Environment.CIRCLE_SHA1 }}
paths:
- "/usr/local/bin"
- "/usr/local/lib/python3.7/site-packages"
- run:
command: |
cd python-packages/order_utils
python setup.py lint
static-tests: static-tests:
working_directory: ~/repo working_directory: ~/repo
docker: docker:
@ -172,7 +257,7 @@ jobs:
- run: yarn lerna run lint - run: yarn lerna run lint
- run: yarn prettier:ci - run: yarn prettier:ci
- run: cd packages/0x.js && yarn build:umd:prod - run: cd packages/0x.js && yarn build:umd:prod
- run: yarn bundlesize - run: yarn bundlewatch
submit-coverage: submit-coverage:
docker: docker:
- image: circleci/node:9 - image: circleci/node:9
@ -232,6 +317,9 @@ jobs:
- restore_cache: - restore_cache:
keys: keys:
- coverage-contracts-{{ .Environment.CIRCLE_SHA1 }} - coverage-contracts-{{ .Environment.CIRCLE_SHA1 }}
- restore_cache:
keys:
- coverage-python-order-utils-{{ .Environment.CIRCLE_SHA1 }}
- run: yarn report_coverage - run: yarn report_coverage
workflows: workflows:
version: 2 version: 2
@ -262,3 +350,8 @@ workflows:
- submit-coverage: - submit-coverage:
requires: requires:
- test-rest - test-rest
- test-python
- test-python
- static-tests-python
# skip python tox run for now, as we don't yet have multiple test environments to support.
#- test-rest-python

10
.gitignore vendored
View File

@ -72,6 +72,7 @@ TODO.md
.vscode .vscode
packages/website/public/bundle* packages/website/public/bundle*
packages/dev-tools-pages/public/bundle*
packages/react-docs/example/public/bundle* packages/react-docs/example/public/bundle*
# server cli # server cli
@ -108,3 +109,12 @@ packages/sol-compiler/solc_bin/
# Monorepo scripts # Monorepo scripts
packages/*/scripts/ packages/*/scripts/
# python stuff
.eggs
.mypy_cache
.tox
python-packages/*/build
__pycache__
python-packages/*/src/*.egg-info
python-packages/*/.coverage

View File

@ -11,7 +11,7 @@
"ganache": "ganache-cli -p 8545 --networkId 50 -m \"${npm_package_config_mnemonic}\"", "ganache": "ganache-cli -p 8545 --networkId 50 -m \"${npm_package_config_mnemonic}\"",
"prettier": "prettier --write '**/*.{ts,tsx,json,md}' --config .prettierrc", "prettier": "prettier --write '**/*.{ts,tsx,json,md}' --config .prettierrc",
"prettier:ci": "prettier --list-different '**/*.{ts,tsx,json,md}' --config .prettierrc", "prettier:ci": "prettier --list-different '**/*.{ts,tsx,json,md}' --config .prettierrc",
"report_coverage": "lcov-result-merger 'packages/*/coverage/lcov.info' | coveralls", "report_coverage": "lcov-result-merger './{packages/*/coverage/lcov.info,python-packages/*/.coverage}' | coveralls",
"test:installation": "node ./packages/monorepo-scripts/lib/test_installation.js", "test:installation": "node ./packages/monorepo-scripts/lib/test_installation.js",
"test:installation:local": "IS_LOCAL_PUBLISH=true node ./packages/monorepo-scripts/lib/test_installation.js", "test:installation:local": "IS_LOCAL_PUBLISH=true node ./packages/monorepo-scripts/lib/test_installation.js",
"test:publish:circleci:comment": "HACK(albrow) We need an automated way to login to npm and echo+sleep piped to stdin was the only way I could find to do it.", "test:publish:circleci:comment": "HACK(albrow) We need an automated way to login to npm and echo+sleep piped to stdin was the only way I could find to do it.",
@ -36,27 +36,35 @@
"test": "wsrun test $PKG --fast-exit --serial --exclude-missing", "test": "wsrun test $PKG --fast-exit --serial --exclude-missing",
"generate_doc": "node ./packages/monorepo-scripts/lib/doc_generate_and_upload.js", "generate_doc": "node ./packages/monorepo-scripts/lib/doc_generate_and_upload.js",
"test:generate_docs:circleci": "for i in ${npm_package_config_packagesWithDocPages}; do yarn generate_doc --package $i --shouldUpload false --isStaging true || break -1; done;", "test:generate_docs:circleci": "for i in ${npm_package_config_packagesWithDocPages}; do yarn generate_doc --package $i --shouldUpload false --isStaging true || break -1; done;",
"bundlesize": "bundlesize", "bundlewatch": "bundlewatch",
"lint": "wsrun lint $PKG --fast-exit --parallel --exclude-missing" "lint": "wsrun lint $PKG --fast-exit --parallel --exclude-missing"
}, },
"config": { "config": {
"mnemonic": "concert load couple harbor equip island argue ramp clarify fence smart topic", "mnemonic": "concert load couple harbor equip island argue ramp clarify fence smart topic",
"packagesWithDocPages": "0x.js connect json-schemas subproviders web3-wrapper contract-wrappers order-utils order-watcher sol-compiler sol-cov ethereum-types" "packagesWithDocPages": "0x.js connect json-schemas subproviders web3-wrapper contract-wrappers order-utils order-watcher sol-compiler sol-cov ethereum-types"
}, },
"bundlesize": [ "bundlewatch" : {
{ "files": [
"path": "packages/0x.js/_bundles/index.min.js" {
}, "path": "packages/0x.js/_bundles/index.min.js",
{ "maxSize": "700kB"
"path": "packages/instant/public/main.bundle.js" },
{
"path": "packages/instant/public/main.bundle.js",
"maxSize": "350kB"
}
],
"ci": {
"trackBranches": ["master", "development"],
"repoBranchBase": "development"
} }
], },
"devDependencies": { "devDependencies": {
"@0x-lerna-fork/lerna": "3.0.0-beta.25", "@0x-lerna-fork/lerna": "3.0.0-beta.25",
"async-child-process": "^1.1.1", "async-child-process": "^1.1.1",
"bundlesize": "^0.17.0", "bundlewatch": "^0.2.1",
"coveralls": "^3.0.0", "coveralls": "^3.0.0",
"ganache-cli": "6.1.3", "ganache-cli": "6.1.8",
"lcov-result-merger": "^3.0.0", "lcov-result-merger": "^3.0.0",
"npm-cli-login": "^0.0.10", "npm-cli-login": "^0.0.10",
"npm-run-all": "^4.1.2", "npm-run-all": "^4.1.2",

View File

@ -1,4 +1,23 @@
[ [
{
"version": "2.0.0",
"changes": [
{
"note": "Add support for `eth_signTypedData`.",
"pr": 1102
},
{
"note":
"Added `MetamaskSubprovider` to handle inconsistencies in Metamask's signing JSON RPC endpoints.",
"pr": 1102
},
{
"note":
"Removed `SignerType` (including `SignerType.Metamask`). Please use the `MetamaskSubprovider` to wrap `web3.currentProvider`.",
"pr": 1102
}
]
},
{ {
"version": "1.0.8", "version": "1.0.8",
"changes": [ "changes": [

View File

@ -84,7 +84,7 @@
"@0xproject/utils": "^2.0.2", "@0xproject/utils": "^2.0.2",
"@0xproject/web3-wrapper": "^3.0.3", "@0xproject/web3-wrapper": "^3.0.3",
"ethereum-types": "^1.0.11", "ethereum-types": "^1.0.11",
"ethers": "4.0.0-beta.14", "ethers": "~4.0.4",
"lodash": "^4.17.5", "lodash": "^4.17.5",
"web3-provider-engine": "14.0.6" "web3-provider-engine": "14.0.6"
}, },

View File

@ -53,7 +53,13 @@ export { OrderWatcher, OnOrderStateChangeCallback, OrderWatcherConfig } from '@0
export import Web3ProviderEngine = require('web3-provider-engine'); export import Web3ProviderEngine = require('web3-provider-engine');
export { RPCSubprovider, Callback, JSONRPCRequestPayloadWithMethod, ErrorCallback } from '@0xproject/subproviders'; export {
RPCSubprovider,
Callback,
JSONRPCRequestPayloadWithMethod,
ErrorCallback,
MetamaskSubprovider,
} from '@0xproject/subproviders';
export { AbiDecoder } from '@0xproject/utils'; export { AbiDecoder } from '@0xproject/utils';
@ -68,7 +74,6 @@ export {
OrderStateInvalid, OrderStateInvalid,
OrderState, OrderState,
AssetProxyId, AssetProxyId,
SignerType,
ERC20AssetData, ERC20AssetData,
ERC721AssetData, ERC721AssetData,
SignatureType, SignatureType,
@ -85,6 +90,7 @@ export {
JSONRPCRequestPayload, JSONRPCRequestPayload,
JSONRPCResponsePayload, JSONRPCResponsePayload,
JSONRPCErrorCallback, JSONRPCErrorCallback,
JSONRPCResponseError,
LogEntry, LogEntry,
DecodedLogArgs, DecodedLogArgs,
LogEntryEvent, LogEntryEvent,

View File

@ -1,4 +1,12 @@
[ [
{
"version": "2.1.0",
"changes": [
{
"note": "Add `gasLimit` and `gasPrice` as optional properties on `BuyQuoteExecutionOpts`"
}
]
},
{ {
"version": "2.0.0", "version": "2.0.0",
"changes": [ "changes": [

View File

@ -183,7 +183,7 @@ export class AssetBuyer {
buyQuote: BuyQuote, buyQuote: BuyQuote,
options: Partial<BuyQuoteExecutionOpts> = {}, options: Partial<BuyQuoteExecutionOpts> = {},
): Promise<string> { ): Promise<string> {
const { ethAmount, takerAddress, feeRecipient } = { const { ethAmount, takerAddress, feeRecipient, gasLimit, gasPrice } = {
...constants.DEFAULT_BUY_QUOTE_EXECUTION_OPTS, ...constants.DEFAULT_BUY_QUOTE_EXECUTION_OPTS,
...options, ...options,
}; };
@ -219,6 +219,10 @@ export class AssetBuyer {
feeOrders, feeOrders,
feePercentage, feePercentage,
feeRecipient, feeRecipient,
{
gasLimit,
gasPrice,
},
); );
return txHash; return txHash;
} }

View File

@ -8,7 +8,7 @@ const MAINNET_NETWORK_ID = 1;
const DEFAULT_ASSET_BUYER_OPTS: AssetBuyerOpts = { const DEFAULT_ASSET_BUYER_OPTS: AssetBuyerOpts = {
networkId: MAINNET_NETWORK_ID, networkId: MAINNET_NETWORK_ID,
orderRefreshIntervalMs: 10000, // 10 seconds orderRefreshIntervalMs: 10000, // 10 seconds
expiryBufferSeconds: 15, expiryBufferSeconds: 300, // 5 minutes
}; };
const DEFAULT_BUY_QUOTE_REQUEST_OPTS: BuyQuoteRequestOpts = { const DEFAULT_BUY_QUOTE_REQUEST_OPTS: BuyQuoteRequestOpts = {

View File

@ -77,18 +77,22 @@ export interface BuyQuoteRequestOpts {
/** /**
* ethAmount: The desired amount of eth to spend. Defaults to buyQuote.worstCaseQuoteInfo.totalEthAmount. * ethAmount: The desired amount of eth to spend. Defaults to buyQuote.worstCaseQuoteInfo.totalEthAmount.
* takerAddress: The address to perform the buy. Defaults to the first available address from the provider. * takerAddress: The address to perform the buy. Defaults to the first available address from the provider.
* gasLimit: The amount of gas to send with a transaction (in Gwei). Defaults to an eth_estimateGas rpc call.
* gasPrice: Gas price in Wei to use for a transaction
* feeRecipient: The address where affiliate fees are sent. Defaults to null address (0x000...000). * feeRecipient: The address where affiliate fees are sent. Defaults to null address (0x000...000).
*/ */
export interface BuyQuoteExecutionOpts { export interface BuyQuoteExecutionOpts {
ethAmount?: BigNumber; ethAmount?: BigNumber;
takerAddress?: string; takerAddress?: string;
gasLimit?: number;
gasPrice?: BigNumber;
feeRecipient: string; feeRecipient: string;
} }
/** /**
* networkId: The ethereum network id. Defaults to 1 (mainnet). * networkId: The ethereum network id. Defaults to 1 (mainnet).
* orderRefreshIntervalMs: The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. Defaults to 10000ms (10s). * orderRefreshIntervalMs: The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. Defaults to 10000ms (10s).
* expiryBufferSeconds: The number of seconds to add when calculating whether an order is expired or not. Defaults to 15s. * expiryBufferSeconds: The number of seconds to add when calculating whether an order is expired or not. Defaults to 300s (5m).
*/ */
export interface AssetBuyerOpts { export interface AssetBuyerOpts {
networkId: number; networkId: number;

View File

@ -72,7 +72,6 @@ export const buyQuoteCalculator = {
assetBuyAmount, assetBuyAmount,
feePercentage, feePercentage,
); );
return { return {
assetData, assetData,
orders: resultOrders, orders: resultOrders,
@ -98,13 +97,14 @@ function calculateQuoteInfo(
); );
// find the total eth needed to buy fees // find the total eth needed to buy fees
const ethAmountToBuyFees = findEthAmountNeededToBuyFees(feeOrdersAndFillableAmounts, zrxAmountToBuyAsset); const ethAmountToBuyFees = findEthAmountNeededToBuyFees(feeOrdersAndFillableAmounts, zrxAmountToBuyAsset);
const ethAmountBeforeAffiliateFee = ethAmountToBuyAsset.plus(ethAmountToBuyFees); const affiliateFeeEthAmount = ethAmountToBuyAsset.mul(feePercentage);
const totalEthAmount = ethAmountBeforeAffiliateFee.mul(feePercentage + 1); const totalEthAmountWithoutAffiliateFee = ethAmountToBuyAsset.plus(ethAmountToBuyFees);
const totalEthAmount = totalEthAmountWithoutAffiliateFee.plus(affiliateFeeEthAmount);
// divide into the assetBuyAmount in order to find rate of makerAsset / WETH // divide into the assetBuyAmount in order to find rate of makerAsset / WETH
const ethPerAssetPrice = ethAmountBeforeAffiliateFee.div(assetBuyAmount); const ethPerAssetPrice = totalEthAmountWithoutAffiliateFee.div(assetBuyAmount);
return { return {
totalEthAmount, totalEthAmount,
feeEthAmount: totalEthAmount.minus(ethAmountBeforeAffiliateFee), feeEthAmount: affiliateFeeEthAmount,
ethPerAssetPrice, ethPerAssetPrice,
}; };
} }

View File

@ -10,7 +10,7 @@ export const orderUtils = {
willOrderExpire(order: SignedOrder, secondsFromNow: number): boolean { willOrderExpire(order: SignedOrder, secondsFromNow: number): boolean {
const millisecondsInSecond = 1000; const millisecondsInSecond = 1000;
const currentUnixTimestampSec = new BigNumber(Date.now() / millisecondsInSecond).round(); const currentUnixTimestampSec = new BigNumber(Date.now() / millisecondsInSecond).round();
return order.expirationTimeSeconds.lessThan(currentUnixTimestampSec.minus(secondsFromNow)); return order.expirationTimeSeconds.lessThan(currentUnixTimestampSec.plus(secondsFromNow));
}, },
calculateRemainingMakerAssetAmount(order: SignedOrder, remainingTakerAssetAmount: BigNumber): BigNumber { calculateRemainingMakerAssetAmount(order: SignedOrder, remainingTakerAssetAmount: BigNumber): BigNumber {
if (remainingTakerAssetAmount.eq(0)) { if (remainingTakerAssetAmount.eq(0)) {

View File

@ -103,9 +103,11 @@ describe('buyQuoteCalculator', () => {
expect(buyQuote.feeOrders).to.deep.equal([smallFeeOrderAndFillableAmount.orders[0]]); expect(buyQuote.feeOrders).to.deep.equal([smallFeeOrderAndFillableAmount.orders[0]]);
// test if rates are correct // test if rates are correct
// 50 eth to fill the first order + 100 eth for fees // 50 eth to fill the first order + 100 eth for fees
const expectedFillEthAmount = new BigNumber(150); const expectedEthAmountForAsset = new BigNumber(50);
const expectedTotalEthAmount = expectedFillEthAmount.mul(feePercentage + 1); const expectedEthAmountForZrxFees = new BigNumber(100);
const expectedFeeEthAmount = expectedTotalEthAmount.minus(expectedFillEthAmount); const expectedFillEthAmount = expectedEthAmountForAsset.plus(expectedEthAmountForZrxFees);
const expectedFeeEthAmount = expectedEthAmountForAsset.mul(feePercentage);
const expectedTotalEthAmount = expectedFillEthAmount.plus(expectedFeeEthAmount);
const expectedEthPerAssetPrice = expectedFillEthAmount.div(assetBuyAmount); const expectedEthPerAssetPrice = expectedFillEthAmount.div(assetBuyAmount);
expect(buyQuote.bestCaseQuoteInfo.feeEthAmount).to.bignumber.equal(expectedFeeEthAmount); expect(buyQuote.bestCaseQuoteInfo.feeEthAmount).to.bignumber.equal(expectedFeeEthAmount);
expect(buyQuote.bestCaseQuoteInfo.totalEthAmount).to.bignumber.equal(expectedTotalEthAmount); expect(buyQuote.bestCaseQuoteInfo.totalEthAmount).to.bignumber.equal(expectedTotalEthAmount);
@ -138,17 +140,21 @@ describe('buyQuoteCalculator', () => {
expect(buyQuote.feeOrders).to.deep.equal(allFeeOrdersAndFillableAmounts.orders); expect(buyQuote.feeOrders).to.deep.equal(allFeeOrdersAndFillableAmounts.orders);
// test if rates are correct // test if rates are correct
// 50 eth to fill the first order + 100 eth for fees // 50 eth to fill the first order + 100 eth for fees
const expectedFillEthAmount = new BigNumber(150); const expectedEthAmountForAsset = new BigNumber(50);
const expectedTotalEthAmount = expectedFillEthAmount.mul(feePercentage + 1); const expectedEthAmountForZrxFees = new BigNumber(100);
const expectedFeeEthAmount = expectedTotalEthAmount.minus(expectedFillEthAmount); const expectedFillEthAmount = expectedEthAmountForAsset.plus(expectedEthAmountForZrxFees);
const expectedFeeEthAmount = expectedEthAmountForAsset.mul(feePercentage);
const expectedTotalEthAmount = expectedFillEthAmount.plus(expectedFeeEthAmount);
const expectedEthPerAssetPrice = expectedFillEthAmount.div(assetBuyAmount); const expectedEthPerAssetPrice = expectedFillEthAmount.div(assetBuyAmount);
expect(buyQuote.bestCaseQuoteInfo.feeEthAmount).to.bignumber.equal(expectedFeeEthAmount); expect(buyQuote.bestCaseQuoteInfo.feeEthAmount).to.bignumber.equal(expectedFeeEthAmount);
expect(buyQuote.bestCaseQuoteInfo.totalEthAmount).to.bignumber.equal(expectedTotalEthAmount); expect(buyQuote.bestCaseQuoteInfo.totalEthAmount).to.bignumber.equal(expectedTotalEthAmount);
expect(buyQuote.bestCaseQuoteInfo.ethPerAssetPrice).to.bignumber.equal(expectedEthPerAssetPrice); expect(buyQuote.bestCaseQuoteInfo.ethPerAssetPrice).to.bignumber.equal(expectedEthPerAssetPrice);
// 100 eth to fill the first order + 200 eth for fees // 100 eth to fill the first order + 200 eth for fees
const expectedWorstFillEthAmount = new BigNumber(300); const expectedWorstEthAmountForAsset = new BigNumber(100);
const expectedWorstTotalEthAmount = expectedWorstFillEthAmount.mul(feePercentage + 1); const expectedWorstEthAmountForZrxFees = new BigNumber(200);
const expectedWorstFeeEthAmount = expectedWorstTotalEthAmount.minus(expectedWorstFillEthAmount); const expectedWorstFillEthAmount = expectedWorstEthAmountForAsset.plus(expectedWorstEthAmountForZrxFees);
const expectedWorstFeeEthAmount = expectedWorstEthAmountForAsset.mul(feePercentage);
const expectedWorstTotalEthAmount = expectedWorstFillEthAmount.plus(expectedWorstFeeEthAmount);
const expectedWorstEthPerAssetPrice = expectedWorstFillEthAmount.div(assetBuyAmount); const expectedWorstEthPerAssetPrice = expectedWorstFillEthAmount.div(assetBuyAmount);
expect(buyQuote.worstCaseQuoteInfo.feeEthAmount).to.bignumber.equal(expectedWorstFeeEthAmount); expect(buyQuote.worstCaseQuoteInfo.feeEthAmount).to.bignumber.equal(expectedWorstFeeEthAmount);
expect(buyQuote.worstCaseQuoteInfo.totalEthAmount).to.bignumber.equal(expectedWorstTotalEthAmount); expect(buyQuote.worstCaseQuoteInfo.totalEthAmount).to.bignumber.equal(expectedWorstTotalEthAmount);

View File

@ -45,7 +45,7 @@
"@0xproject/utils": "^2.0.2", "@0xproject/utils": "^2.0.2",
"@0xproject/web3-wrapper": "^3.0.3", "@0xproject/web3-wrapper": "^3.0.3",
"ethereum-types": "^1.0.11", "ethereum-types": "^1.0.11",
"ethers": "4.0.0-beta.14", "ethers": "~4.0.4",
"lodash": "^4.17.5" "lodash": "^4.17.5"
}, },
"publishConfig": { "publishConfig": {

View File

@ -17,7 +17,7 @@ import * as _ from 'lodash';
import { formatABIDataItem } from './utils'; import { formatABIDataItem } from './utils';
export interface EthersInterfaceByFunctionSignature { export interface EthersInterfaceByFunctionSignature {
[key: string]: ethers.Interface; [key: string]: ethers.utils.Interface;
} }
const REVERT_ERROR_SELECTOR = '08c379a0'; const REVERT_ERROR_SELECTOR = '08c379a0';
@ -101,7 +101,7 @@ export class BaseContract {
// if it overflows the corresponding Solidity type, there is a bug in the // if it overflows the corresponding Solidity type, there is a bug in the
// encoder, or the encoder performs unsafe type coercion. // encoder, or the encoder performs unsafe type coercion.
public static strictArgumentEncodingCheck(inputAbi: DataItem[], args: any[]): void { public static strictArgumentEncodingCheck(inputAbi: DataItem[], args: any[]): void {
const coder = new ethers.AbiCoder(); const coder = new ethers.utils.AbiCoder();
const params = abiUtils.parseEthersParams(inputAbi); const params = abiUtils.parseEthersParams(inputAbi);
const rawEncoded = coder.encode(inputAbi, args); const rawEncoded = coder.encode(inputAbi, args);
const rawDecoded = coder.decode(inputAbi, rawEncoded); const rawDecoded = coder.decode(inputAbi, rawEncoded);
@ -117,7 +117,7 @@ export class BaseContract {
} }
} }
} }
protected _lookupEthersInterface(functionSignature: string): ethers.Interface { protected _lookupEthersInterface(functionSignature: string): ethers.utils.Interface {
const ethersInterface = this._ethersInterfacesByFunctionSignature[functionSignature]; const ethersInterface = this._ethersInterfacesByFunctionSignature[functionSignature];
if (_.isUndefined(ethersInterface)) { if (_.isUndefined(ethersInterface)) {
throw new Error(`Failed to lookup method with function signature '${functionSignature}'`); throw new Error(`Failed to lookup method with function signature '${functionSignature}'`);
@ -154,7 +154,7 @@ export class BaseContract {
this._ethersInterfacesByFunctionSignature = {}; this._ethersInterfacesByFunctionSignature = {};
_.each(methodAbis, methodAbi => { _.each(methodAbis, methodAbi => {
const functionSignature = abiUtils.getFunctionSignature(methodAbi); const functionSignature = abiUtils.getFunctionSignature(methodAbi);
this._ethersInterfacesByFunctionSignature[functionSignature] = new ethers.Interface([methodAbi]); this._ethersInterfacesByFunctionSignature[functionSignature] = new ethers.utils.Interface([methodAbi]);
}); });
} }
} }

View File

@ -84,7 +84,7 @@
"ethereum-types": "^1.0.11", "ethereum-types": "^1.0.11",
"ethereumjs-blockstream": "6.0.0", "ethereumjs-blockstream": "6.0.0",
"ethereumjs-util": "^5.1.1", "ethereumjs-util": "^5.1.1",
"ethers": "4.0.0-beta.14", "ethers": "~4.0.4",
"js-sha3": "^0.7.0", "js-sha3": "^0.7.0",
"lodash": "^4.17.5", "lodash": "^4.17.5",
"uuid": "^3.1.0" "uuid": "^3.1.0"

View File

@ -39,6 +39,7 @@ export {
JSONRPCRequestPayload, JSONRPCRequestPayload,
JSONRPCResponsePayload, JSONRPCResponsePayload,
JSONRPCErrorCallback, JSONRPCErrorCallback,
JSONRPCResponseError,
AbiDefinition, AbiDefinition,
LogWithDecodedArgs, LogWithDecodedArgs,
FunctionAbi, FunctionAbi,

View File

@ -1,22 +1,13 @@
import { schemas } from '@0xproject/json-schemas'; import { schemas } from '@0xproject/json-schemas';
import { EIP712Schema, EIP712Types, eip712Utils } from '@0xproject/order-utils'; import { eip712Utils } from '@0xproject/order-utils';
import { Order, SignedOrder } from '@0xproject/types'; import { Order, SignedOrder } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils'; import { BigNumber, signTypedDataUtils } from '@0xproject/utils';
import _ = require('lodash'); import _ = require('lodash');
import { ExchangeContract } from '../contract_wrappers/generated/exchange'; import { ExchangeContract } from '../contract_wrappers/generated/exchange';
import { assert } from './assert'; import { assert } from './assert';
const EIP712_ZEROEX_TRANSACTION_SCHEMA: EIP712Schema = {
name: 'ZeroExTransaction',
parameters: [
{ name: 'salt', type: EIP712Types.Uint256 },
{ name: 'signerAddress', type: EIP712Types.Address },
{ name: 'data', type: EIP712Types.Bytes },
],
};
/** /**
* Transaction Encoder. Transaction messages exist for the purpose of calling methods on the Exchange contract * Transaction Encoder. Transaction messages exist for the purpose of calling methods on the Exchange contract
* in the context of another address. For example, UserA can encode and sign a fillOrder transaction and UserB * in the context of another address. For example, UserA can encode and sign a fillOrder transaction and UserB
@ -41,12 +32,9 @@ export class TransactionEncoder {
signerAddress, signerAddress,
data, data,
}; };
const executeTransactionHashBuff = eip712Utils.structHash( const typedData = eip712Utils.createZeroExTransactionTypedData(executeTransactionData, exchangeAddress);
EIP712_ZEROEX_TRANSACTION_SCHEMA, const eip712MessageBuffer = signTypedDataUtils.generateTypedDataHash(typedData);
executeTransactionData, const messageHex = `0x${eip712MessageBuffer.toString('hex')}`;
);
const eip721MessageBuffer = eip712Utils.createEIP712Message(executeTransactionHashBuff, exchangeAddress);
const messageHex = `0x${eip721MessageBuffer.toString('hex')}`;
return messageHex; return messageHex;
} }
/** /**

View File

@ -1,7 +1,7 @@
import { BlockchainLifecycle } from '@0xproject/dev-utils'; import { BlockchainLifecycle } from '@0xproject/dev-utils';
import { FillScenarios } from '@0xproject/fill-scenarios'; import { FillScenarios } from '@0xproject/fill-scenarios';
import { assetDataUtils, generatePseudoRandomSalt, orderHashUtils, signatureUtils } from '@0xproject/order-utils'; import { assetDataUtils, generatePseudoRandomSalt, orderHashUtils, signatureUtils } from '@0xproject/order-utils';
import { SignedOrder, SignerType } from '@0xproject/types'; import { SignedOrder } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils'; import { BigNumber } from '@0xproject/utils';
import 'mocha'; import 'mocha';
@ -80,12 +80,7 @@ describe('TransactionEncoder', () => {
): Promise<void> => { ): Promise<void> => {
const salt = generatePseudoRandomSalt(); const salt = generatePseudoRandomSalt();
const encodedTransaction = encoder.getTransactionHex(data, salt, signerAddress); const encodedTransaction = encoder.getTransactionHex(data, salt, signerAddress);
const signature = await signatureUtils.ecSignOrderHashAsync( const signature = await signatureUtils.ecSignHashAsync(provider, encodedTransaction, signerAddress);
provider,
encodedTransaction,
signerAddress,
SignerType.Default,
);
txHash = await contractWrappers.exchange.executeTransactionAsync( txHash = await contractWrappers.exchange.executeTransactionAsync(
salt, salt,
signerAddress, signerAddress,

View File

@ -65,7 +65,7 @@ export class {{contractName}}Contract extends BaseContract {
[{{> params inputs=ctor.inputs}}], [{{> params inputs=ctor.inputs}}],
BaseContract._bigNumberToString, BaseContract._bigNumberToString,
); );
const iface = new ethers.Interface(abi); const iface = new ethers.utils.Interface(abi);
const deployInfo = iface.deployFunction; const deployInfo = iface.deployFunction;
const txData = deployInfo.encode(bytecode, [{{> params inputs=ctor.inputs}}]); const txData = deployInfo.encode(bytecode, [{{> params inputs=ctor.inputs}}]);
const web3Wrapper = new Web3Wrapper(provider); const web3Wrapper = new Web3Wrapper(provider);

View File

@ -84,7 +84,7 @@
"ethereum-types": "^1.0.11", "ethereum-types": "^1.0.11",
"ethereumjs-abi": "0.6.5", "ethereumjs-abi": "0.6.5",
"ethereumjs-util": "^5.1.1", "ethereumjs-util": "^5.1.1",
"ethers": "4.0.0-beta.14", "ethers": "~4.0.4",
"js-combinatorics": "^0.5.3", "js-combinatorics": "^0.5.3",
"lodash": "^4.17.5" "lodash": "^4.17.5"
} }

View File

@ -1,5 +1,5 @@
import { BlockchainLifecycle } from '@0xproject/dev-utils'; import { BlockchainLifecycle } from '@0xproject/dev-utils';
import { assetDataUtils, eip712Utils, orderHashUtils } from '@0xproject/order-utils'; import { assetDataUtils, orderHashUtils } from '@0xproject/order-utils';
import { SignedOrder } from '@0xproject/types'; import { SignedOrder } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils'; import { BigNumber } from '@0xproject/utils';
import * as chai from 'chai'; import * as chai from 'chai';
@ -126,22 +126,6 @@ describe('Exchange libs', () => {
}); });
describe('LibOrder', () => { describe('LibOrder', () => {
describe('getOrderSchema', () => {
it('should output the correct order schema hash', async () => {
const orderSchema = await libs.getOrderSchemaHash.callAsync();
const schemaHashBuffer = orderHashUtils._getOrderSchemaBuffer();
const schemaHashHex = `0x${schemaHashBuffer.toString('hex')}`;
expect(schemaHashHex).to.be.equal(orderSchema);
});
});
describe('getDomainSeparatorSchema', () => {
it('should output the correct domain separator schema hash', async () => {
const domainSeparatorSchema = await libs.getDomainSeparatorSchemaHash.callAsync();
const domainSchemaBuffer = eip712Utils._getDomainSeparatorSchemaBuffer();
const schemaHashHex = `0x${domainSchemaBuffer.toString('hex')}`;
expect(schemaHashHex).to.be.equal(domainSeparatorSchema);
});
});
describe('getOrderHash', () => { describe('getOrderHash', () => {
it('should output the correct orderHash', async () => { it('should output the correct orderHash', async () => {
signedOrder = await orderFactory.newSignedOrderAsync(); signedOrder = await orderFactory.newSignedOrderAsync();

View File

@ -1,6 +1,6 @@
import { BlockchainLifecycle } from '@0xproject/dev-utils'; import { BlockchainLifecycle } from '@0xproject/dev-utils';
import { assetDataUtils, orderHashUtils, signatureUtils } from '@0xproject/order-utils'; import { assetDataUtils, orderHashUtils, signatureUtils } from '@0xproject/order-utils';
import { RevertReason, SignatureType, SignedOrder, SignerType } from '@0xproject/types'; import { RevertReason, SignatureType, SignedOrder } from '@0xproject/types';
import * as chai from 'chai'; import * as chai from 'chai';
import { LogWithDecodedArgs } from 'ethereum-types'; import { LogWithDecodedArgs } from 'ethereum-types';
import ethUtil = require('ethereumjs-util'); import ethUtil = require('ethereumjs-util');
@ -231,10 +231,7 @@ describe('MixinSignatureValidator', () => {
it('should return true when SignatureType=EthSign and signature is valid', async () => { it('should return true when SignatureType=EthSign and signature is valid', async () => {
// Create EthSign signature // Create EthSign signature
const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
const orderHashWithEthSignPrefixHex = signatureUtils.addSignedMessagePrefix( const orderHashWithEthSignPrefixHex = signatureUtils.addSignedMessagePrefix(orderHashHex);
orderHashHex,
SignerType.Default,
);
const orderHashWithEthSignPrefixBuffer = ethUtil.toBuffer(orderHashWithEthSignPrefixHex); const orderHashWithEthSignPrefixBuffer = ethUtil.toBuffer(orderHashWithEthSignPrefixHex);
const ecSignature = ethUtil.ecsign(orderHashWithEthSignPrefixBuffer, signerPrivateKey); const ecSignature = ethUtil.ecsign(orderHashWithEthSignPrefixBuffer, signerPrivateKey);
// Create 0x signature from EthSign signature // Create 0x signature from EthSign signature
@ -257,10 +254,7 @@ describe('MixinSignatureValidator', () => {
it('should return false when SignatureType=EthSign and signature is invalid', async () => { it('should return false when SignatureType=EthSign and signature is invalid', async () => {
// Create EthSign signature // Create EthSign signature
const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder); const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
const orderHashWithEthSignPrefixHex = signatureUtils.addSignedMessagePrefix( const orderHashWithEthSignPrefixHex = signatureUtils.addSignedMessagePrefix(orderHashHex);
orderHashHex,
SignerType.Default,
);
const orderHashWithEthSignPrefixBuffer = ethUtil.toBuffer(orderHashWithEthSignPrefixHex); const orderHashWithEthSignPrefixBuffer = ethUtil.toBuffer(orderHashWithEthSignPrefixHex);
const ecSignature = ethUtil.ecsign(orderHashWithEthSignPrefixBuffer, signerPrivateKey); const ecSignature = ethUtil.ecsign(orderHashWithEthSignPrefixBuffer, signerPrivateKey);
// Create 0x signature from EthSign signature // Create 0x signature from EthSign signature

View File

@ -1,19 +1,11 @@
import { EIP712Schema, EIP712Types, eip712Utils, generatePseudoRandomSalt } from '@0xproject/order-utils'; import { eip712Utils, generatePseudoRandomSalt } from '@0xproject/order-utils';
import { SignatureType } from '@0xproject/types'; import { SignatureType } from '@0xproject/types';
import { signTypedDataUtils } from '@0xproject/utils';
import * as ethUtil from 'ethereumjs-util'; import * as ethUtil from 'ethereumjs-util';
import { signingUtils } from './signing_utils'; import { signingUtils } from './signing_utils';
import { SignedTransaction } from './types'; import { SignedTransaction } from './types';
const EIP712_ZEROEX_TRANSACTION_SCHEMA: EIP712Schema = {
name: 'ZeroExTransaction',
parameters: [
{ name: 'salt', type: EIP712Types.Uint256 },
{ name: 'signerAddress', type: EIP712Types.Address },
{ name: 'data', type: EIP712Types.Bytes },
],
};
export class TransactionFactory { export class TransactionFactory {
private readonly _signerBuff: Buffer; private readonly _signerBuff: Buffer;
private readonly _exchangeAddress: string; private readonly _exchangeAddress: string;
@ -31,12 +23,10 @@ export class TransactionFactory {
signerAddress, signerAddress,
data, data,
}; };
const executeTransactionHashBuff = eip712Utils.structHash(
EIP712_ZEROEX_TRANSACTION_SCHEMA, const typedData = eip712Utils.createZeroExTransactionTypedData(executeTransactionData, this._exchangeAddress);
executeTransactionData, const eip712MessageBuffer = signTypedDataUtils.generateTypedDataHash(typedData);
); const signature = signingUtils.signMessage(eip712MessageBuffer, this._privateKey, signatureType);
const txHash = eip712Utils.createEIP712Message(executeTransactionHashBuff, this._exchangeAddress);
const signature = signingUtils.signMessage(txHash, this._privateKey, signatureType);
const signedTx = { const signedTx = {
exchangeAddress: this._exchangeAddress, exchangeAddress: this._exchangeAddress,
signature: `0x${signature.toString('hex')}`, signature: `0x${signature.toString('hex')}`,

View File

@ -0,0 +1,88 @@
## Dev tools pages
This repository contains our dev tools pages.
## Local Dev Setup
Requires Node version 6.9.5 or higher & yarn v1.9.4
### 1. Install dependencies for monorepo:
Make sure you install Yarn v1.9.4 (npm won't work!). We rely on our `yarn.lock` file and on Yarn's support for `workspaces` in our monorepo setup.
```bash
yarn install
```
### 2. Initial setup
To build this package and all other monorepo packages that it depends on, run the following from the monorepo root directory:
```bash
PKG=@0xproject/dev-tools-pages yarn build
```
Note: Ignore the `WARNING in asset size limit` and `WARNING in entrypoint size limit` warnings.
### 3. Run dev server
```bash
cd packages/dev-tools-pages
yarn dev
```
Visit [http://localhost:3572/](http://localhost:3572/) in your browser.
The webpage will refresh when source code is changed.
### 4. Code!
There are some basic primitives we'd like you to use:
1. `<Container>Stuff</Container>`: Use containers instead of divs,spans,etc... and use it's props instead of inline styles (e.g `style={{margin: 3}}` should be `margin="3px"`
2. `<Text>Look ma, text!</Text>`: Use text components whenever rendering text. It has props for manipulating texts, so again no in-line styles. Use `fontColor="red"`, not `style={{color: 'red'}}`.
3. Styled-components: See the `ui/button.tsx` file for an example of how to use these.
4. BassCss: This library gives you access to a bunch of [classes](http://basscss.com/) that apply styles in a browser-compatible way, has affordances for responsiveness and alleviates the need for inline styles or LESS/CSS files.
With the above 4 tools and following the React paradigm, you shouldn't need CSS/LESS files. IF there are special occasions where you do, these is a `all.less` file, but this is a solution of last resort. Use it sparingly.
### Clean
```bash
yarn clean
```
### Lint
```bash
yarn lint
```
### Prettier
Run from the monorepo root directory:
```
yarn prettier
```
### Resources
##### Toolkit
* [Styled Components](https://www.styled-components.com/)
* [BassCSS](http://basscss.com/)
##### Recommended Atom packages:
* [atom-typescript](https://atom.io/packages/atom-typescript)
* [linter-tslint](https://atom.io/packages/linter-tslint)
## Contributing
We strongly recommend that the community help us make improvements and determine the future direction of the protocol. To report bugs within this package, please create an issue in this repository.
Please read our [contribution guidelines](../../CONTRIBUTING.md) before getting started.

View File

View File

@ -0,0 +1,58 @@
{
"name": "@0xproject/dev-tools-pages",
"version": "0.0.1",
"engines": {
"node": ">=6.12"
},
"private": true,
"description": "0x Dev tools pages",
"scripts": {
"build": "node --max_old_space_size=8192 ../../node_modules/.bin/webpack --mode production",
"build:ci": "yarn build",
"build:dev": "../../node_modules/.bin/webpack --mode development",
"clean": "shx rm -f public/bundle*",
"lint": "tslint --project . 'ts/**/*.ts' 'ts/**/*.tsx'",
"dev": "webpack-dev-server --mode development --content-base public"
},
"license": "Apache-2.0",
"dependencies": {
"@0xproject/react-shared": "^1.0.15",
"basscss": "^8.0.3",
"bowser": "^1.9.3",
"less": "^2.7.2",
"lodash": "^4.17.5",
"polished": "^1.9.2",
"react": "^16.4.2",
"react-document-title": "^2.0.3",
"react-dom": "^16.4.2",
"react-helmet": "^5.2.0",
"styled-components": "^3.3.0"
},
"devDependencies": {
"@types/lodash": "4.14.104",
"@types/node": "*",
"@types/react": "^16.4.2",
"@types/react-dom": "^16.0.7",
"@types/react-helmet": "^5.0.6",
"@types/react-router-dom": "^4.0.4",
"@types/react-tap-event-plugin": "0.0.30",
"@types/styled-components": "^4.0.0",
"awesome-typescript-loader": "^5.2.1",
"copyfiles": "^2.0.0",
"css-loader": "0.23.x",
"less-loader": "^4.1.0",
"make-promises-safe": "^1.1.0",
"raw-loader": "^0.5.1",
"shx": "^0.2.2",
"source-map-loader": "^0.2.4",
"style-loader": "0.23.x",
"terser-webpack-plugin": "^1.1.0",
"tslint": "5.11.0",
"tslint-config-0xproject": "^0.0.2",
"typescript": "3.0.1",
"uglifyjs-webpack-plugin": "^2.0.1",
"webpack": "^4.20.2",
"webpack-cli": "3.1.2",
"webpack-dev-server": "^3.1.9"
}
}

View File

@ -0,0 +1,85 @@
/* Custom Basscss Responsive Utilities */
@media (max-width: 52em) {
.sm-center {
text-align: center;
}
.sm-align-middle {
vertical-align: middle;
}
.sm-align-top {
vertical-align: top;
}
.sm-left-align {
text-align: left;
}
.sm-right-align {
text-align: right;
}
.sm-table-cell {
display: table-cell;
}
.sm-mx-auto {
margin-left: auto;
margin-right: auto;
}
.sm-right {
float: right;
}
}
@media (min-width: 52em) {
.md-center {
text-align: center;
}
.md-align-middle {
vertical-align: middle;
}
.md-align-top {
vertical-align: top;
}
.md-left-align {
text-align: left;
}
.md-right-align {
text-align: right;
}
.md-table-cell {
display: table-cell;
}
.md-mx-auto {
margin-left: auto;
margin-right: auto;
}
.md-right {
float: right;
}
}
@media (min-width: 64em) {
.lg-center {
text-align: center;
}
.lg-align-middle {
vertical-align: middle;
}
.lg-align-top {
vertical-align: top;
}
.lg-left-align {
text-align: left;
}
.lg-right-align {
text-align: right;
}
.lg-table-cell {
display: table-cell;
}
.lg-mx-auto {
margin-left: auto;
margin-right: auto;
}
.lg-right {
float: right;
}
}

View File

@ -0,0 +1,453 @@
/* Basscss Responsive Margin */
@media (max-width: 52em) {
/* Modified by Fabio Berger to max-width from min-width */
.sm-m0 {
margin: 0;
}
.sm-mt0 {
margin-top: 0;
}
.sm-mr0 {
margin-right: 0;
}
.sm-mb0 {
margin-bottom: 0;
}
.sm-ml0 {
margin-left: 0;
}
.sm-mx0 {
margin-left: 0;
margin-right: 0;
}
.sm-my0 {
margin-top: 0;
margin-bottom: 0;
}
.sm-m1 {
margin: 0.5rem;
}
.sm-mt1 {
margin-top: 0.5rem;
}
.sm-mr1 {
margin-right: 0.5rem;
}
.sm-mb1 {
margin-bottom: 0.5rem;
}
.sm-ml1 {
margin-left: 0.5rem;
}
.sm-mx1 {
margin-left: 0.5rem;
margin-right: 0.5rem;
}
.sm-my1 {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.sm-m2 {
margin: 1rem;
}
.sm-mt2 {
margin-top: 1rem;
}
.sm-mr2 {
margin-right: 1rem;
}
.sm-mb2 {
margin-bottom: 1rem;
}
.sm-ml2 {
margin-left: 1rem;
}
.sm-mx2 {
margin-left: 1rem;
margin-right: 1rem;
}
.sm-my2 {
margin-top: 1rem;
margin-bottom: 1rem;
}
.sm-m3 {
margin: 2rem;
}
.sm-mt3 {
margin-top: 2rem;
}
.sm-mr3 {
margin-right: 2rem;
}
.sm-mb3 {
margin-bottom: 2rem;
}
.sm-ml3 {
margin-left: 2rem;
}
.sm-mx3 {
margin-left: 2rem;
margin-right: 2rem;
}
.sm-my3 {
margin-top: 2rem;
margin-bottom: 2rem;
}
.sm-m4 {
margin: 4rem;
}
.sm-mt4 {
margin-top: 4rem;
}
.sm-mr4 {
margin-right: 4rem;
}
.sm-mb4 {
margin-bottom: 4rem;
}
.sm-ml4 {
margin-left: 4rem;
}
.sm-mx4 {
margin-left: 4rem;
margin-right: 4rem;
}
.sm-my4 {
margin-top: 4rem;
margin-bottom: 4rem;
}
.sm-mxn1 {
margin-left: -0.5rem;
margin-right: -0.5rem;
}
.sm-mxn2 {
margin-left: -1rem;
margin-right: -1rem;
}
.sm-mxn3 {
margin-left: -2rem;
margin-right: -2rem;
}
.sm-mxn4 {
margin-left: -4rem;
margin-right: -4rem;
}
.sm-ml-auto {
margin-left: auto;
}
.sm-mr-auto {
margin-right: auto;
}
.sm-mx-auto {
margin-left: auto;
margin-right: auto;
}
}
@media (min-width: 52em) {
.md-m0 {
margin: 0;
}
.md-mt0 {
margin-top: 0;
}
.md-mr0 {
margin-right: 0;
}
.md-mb0 {
margin-bottom: 0;
}
.md-ml0 {
margin-left: 0;
}
.md-mx0 {
margin-left: 0;
margin-right: 0;
}
.md-my0 {
margin-top: 0;
margin-bottom: 0;
}
.md-m1 {
margin: 0.5rem;
}
.md-mt1 {
margin-top: 0.5rem;
}
.md-mr1 {
margin-right: 0.5rem;
}
.md-mb1 {
margin-bottom: 0.5rem;
}
.md-ml1 {
margin-left: 0.5rem;
}
.md-mx1 {
margin-left: 0.5rem;
margin-right: 0.5rem;
}
.md-my1 {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.md-m2 {
margin: 1rem;
}
.md-mt2 {
margin-top: 1rem;
}
.md-mr2 {
margin-right: 1rem;
}
.md-mb2 {
margin-bottom: 1rem;
}
.md-ml2 {
margin-left: 1rem;
}
.md-mx2 {
margin-left: 1rem;
margin-right: 1rem;
}
.md-my2 {
margin-top: 1rem;
margin-bottom: 1rem;
}
.md-m3 {
margin: 2rem;
}
.md-mt3 {
margin-top: 2rem;
}
.md-mr3 {
margin-right: 2rem;
}
.md-mb3 {
margin-bottom: 2rem;
}
.md-ml3 {
margin-left: 2rem;
}
.md-mx3 {
margin-left: 2rem;
margin-right: 2rem;
}
.md-my3 {
margin-top: 2rem;
margin-bottom: 2rem;
}
.md-m4 {
margin: 4rem;
}
.md-mt4 {
margin-top: 4rem;
}
.md-mr4 {
margin-right: 4rem;
}
.md-mb4 {
margin-bottom: 4rem;
}
.md-ml4 {
margin-left: 4rem;
}
.md-mx4 {
margin-left: 4rem;
margin-right: 4rem;
}
.md-my4 {
margin-top: 4rem;
margin-bottom: 4rem;
}
.md-mxn1 {
margin-left: -0.5rem;
margin-right: -0.5rem;
}
.md-mxn2 {
margin-left: -1rem;
margin-right: -1rem;
}
.md-mxn3 {
margin-left: -2rem;
margin-right: -2rem;
}
.md-mxn4 {
margin-left: -4rem;
margin-right: -4rem;
}
.md-ml-auto {
margin-left: auto;
}
.md-mr-auto {
margin-right: auto;
}
.md-mx-auto {
margin-left: auto;
margin-right: auto;
}
}
@media (min-width: 64em) {
.lg-m0 {
margin: 0;
}
.lg-mt0 {
margin-top: 0;
}
.lg-mr0 {
margin-right: 0;
}
.lg-mb0 {
margin-bottom: 0;
}
.lg-ml0 {
margin-left: 0;
}
.lg-mx0 {
margin-left: 0;
margin-right: 0;
}
.lg-my0 {
margin-top: 0;
margin-bottom: 0;
}
.lg-m1 {
margin: 0.5rem;
}
.lg-mt1 {
margin-top: 0.5rem;
}
.lg-mr1 {
margin-right: 0.5rem;
}
.lg-mb1 {
margin-bottom: 0.5rem;
}
.lg-ml1 {
margin-left: 0.5rem;
}
.lg-mx1 {
margin-left: 0.5rem;
margin-right: 0.5rem;
}
.lg-my1 {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.lg-m2 {
margin: 1rem;
}
.lg-mt2 {
margin-top: 1rem;
}
.lg-mr2 {
margin-right: 1rem;
}
.lg-mb2 {
margin-bottom: 1rem;
}
.lg-ml2 {
margin-left: 1rem;
}
.lg-mx2 {
margin-left: 1rem;
margin-right: 1rem;
}
.lg-my2 {
margin-top: 1rem;
margin-bottom: 1rem;
}
.lg-m3 {
margin: 2rem;
}
.lg-mt3 {
margin-top: 2rem;
}
.lg-mr3 {
margin-right: 2rem;
}
.lg-mb3 {
margin-bottom: 2rem;
}
.lg-ml3 {
margin-left: 2rem;
}
.lg-mx3 {
margin-left: 2rem;
margin-right: 2rem;
}
.lg-my3 {
margin-top: 2rem;
margin-bottom: 2rem;
}
.lg-m4 {
margin: 4rem;
}
.lg-mt4 {
margin-top: 4rem;
}
.lg-mr4 {
margin-right: 4rem;
}
.lg-mb4 {
margin-bottom: 4rem;
}
.lg-ml4 {
margin-left: 4rem;
}
.lg-mx4 {
margin-left: 4rem;
margin-right: 4rem;
}
.lg-my4 {
margin-top: 4rem;
margin-bottom: 4rem;
}
.lg-mxn1 {
margin-left: -0.5rem;
margin-right: -0.5rem;
}
.lg-mxn2 {
margin-left: -1rem;
margin-right: -1rem;
}
.lg-mxn3 {
margin-left: -2rem;
margin-right: -2rem;
}
.lg-mxn4 {
margin-left: -4rem;
margin-right: -4rem;
}
.lg-ml-auto {
margin-left: auto;
}
.lg-mr-auto {
margin-right: auto;
}
.lg-mx-auto {
margin-left: auto;
margin-right: auto;
}
}

View File

@ -0,0 +1,134 @@
/* Basscss Responsive Padding */
/* Modified by Fabio Berger to include xs prefix */
@media (max-width: 52em) { /* Modified by Fabio Berger to max-width from min-width */
.sm-p0 { padding: 0 }
.sm-pt0 { padding-top: 0 }
.sm-pr0 { padding-right: 0 }
.sm-pb0 { padding-bottom: 0 }
.sm-pl0 { padding-left: 0 }
.sm-px0 { padding-left: 0; padding-right: 0 }
.sm-py0 { padding-top: 0; padding-bottom: 0 }
.sm-p1 { padding: .5rem }
.sm-pt1 { padding-top: .5rem }
.sm-pr1 { padding-right: .5rem }
.sm-pb1 { padding-bottom: .5rem }
.sm-pl1 { padding-left: .5rem }
.sm-px1 { padding-left: .5rem; padding-right: .5rem }
.sm-py1 { padding-top: .5rem; padding-bottom: .5rem }
.sm-p2 { padding: 1rem }
.sm-pt2 { padding-top: 1rem }
.sm-pr2 { padding-right: 1rem }
.sm-pb2 { padding-bottom: 1rem }
.sm-pl2 { padding-left: 1rem }
.sm-px2 { padding-left: 1rem; padding-right: 1rem }
.sm-py2 { padding-top: 1rem; padding-bottom: 1rem }
.sm-p3 { padding: 2rem }
.sm-pt3 { padding-top: 2rem }
.sm-pr3 { padding-right: 2rem }
.sm-pb3 { padding-bottom: 2rem }
.sm-pl3 { padding-left: 2rem }
.sm-px3 { padding-left: 2rem; padding-right: 2rem }
.sm-py3 { padding-top: 2rem; padding-bottom: 2rem }
.sm-p4 { padding: 4rem }
.sm-pt4 { padding-top: 4rem }
.sm-pr4 { padding-right: 4rem }
.sm-pb4 { padding-bottom: 4rem }
.sm-pl4 { padding-left: 4rem }
.sm-px4 { padding-left: 4rem; padding-right: 4rem }
.sm-py4 { padding-top: 4rem; padding-bottom: 4rem }
}
@media (min-width: 52em) {
.md-p0 { padding: 0 }
.md-pt0 { padding-top: 0 }
.md-pr0 { padding-right: 0 }
.md-pb0 { padding-bottom: 0 }
.md-pl0 { padding-left: 0 }
.md-px0 { padding-left: 0; padding-right: 0 }
.md-py0 { padding-top: 0; padding-bottom: 0 }
.md-p1 { padding: .5rem }
.md-pt1 { padding-top: .5rem }
.md-pr1 { padding-right: .5rem }
.md-pb1 { padding-bottom: .5rem }
.md-pl1 { padding-left: .5rem }
.md-px1 { padding-left: .5rem; padding-right: .5rem }
.md-py1 { padding-top: .5rem; padding-bottom: .5rem }
.md-p2 { padding: 1rem }
.md-pt2 { padding-top: 1rem }
.md-pr2 { padding-right: 1rem }
.md-pb2 { padding-bottom: 1rem }
.md-pl2 { padding-left: 1rem }
.md-px2 { padding-left: 1rem; padding-right: 1rem }
.md-py2 { padding-top: 1rem; padding-bottom: 1rem }
.md-p3 { padding: 2rem }
.md-pt3 { padding-top: 2rem }
.md-pr3 { padding-right: 2rem }
.md-pb3 { padding-bottom: 2rem }
.md-pl3 { padding-left: 2rem }
.md-px3 { padding-left: 2rem; padding-right: 2rem }
.md-py3 { padding-top: 2rem; padding-bottom: 2rem }
.md-p4 { padding: 4rem }
.md-pt4 { padding-top: 4rem }
.md-pr4 { padding-right: 4rem }
.md-pb4 { padding-bottom: 4rem }
.md-pl4 { padding-left: 4rem }
.md-px4 { padding-left: 4rem; padding-right: 4rem }
.md-py4 { padding-top: 4rem; padding-bottom: 4rem }
}
@media (min-width: 64em) {
.lg-p0 { padding: 0 }
.lg-pt0 { padding-top: 0 }
.lg-pr0 { padding-right: 0 }
.lg-pb0 { padding-bottom: 0 }
.lg-pl0 { padding-left: 0 }
.lg-px0 { padding-left: 0; padding-right: 0 }
.lg-py0 { padding-top: 0; padding-bottom: 0 }
.lg-p1 { padding: .5rem }
.lg-pt1 { padding-top: .5rem }
.lg-pr1 { padding-right: .5rem }
.lg-pb1 { padding-bottom: .5rem }
.lg-pl1 { padding-left: .5rem }
.lg-px1 { padding-left: .5rem; padding-right: .5rem }
.lg-py1 { padding-top: .5rem; padding-bottom: .5rem }
.lg-p2 { padding: 1rem }
.lg-pt2 { padding-top: 1rem }
.lg-pr2 { padding-right: 1rem }
.lg-pb2 { padding-bottom: 1rem }
.lg-pl2 { padding-left: 1rem }
.lg-px2 { padding-left: 1rem; padding-right: 1rem }
.lg-py2 { padding-top: 1rem; padding-bottom: 1rem }
.lg-p3 { padding: 2rem }
.lg-pt3 { padding-top: 2rem }
.lg-pr3 { padding-right: 2rem }
.lg-pb3 { padding-bottom: 2rem }
.lg-pl3 { padding-left: 2rem }
.lg-px3 { padding-left: 2rem; padding-right: 2rem }
.lg-py3 { padding-top: 2rem; padding-bottom: 2rem }
.lg-p4 { padding: 4rem }
.lg-pt4 { padding-top: 4rem }
.lg-pr4 { padding-right: 4rem }
.lg-pb4 { padding-bottom: 4rem }
.lg-pl4 { padding-left: 4rem }
.lg-px4 { padding-left: 4rem; padding-right: 4rem }
.lg-py4 { padding-top: 4rem; padding-bottom: 4rem }
}

View File

@ -0,0 +1,35 @@
/* Basscss Responsive Type Scale */
/* Modified by Fabio Berger to include xs prefix */
@media (max-width: 52em) { /* Modified by Fabio Berger to max-width from min-width */
.sm-h00 { font-size: 4rem }
.sm-h0 { font-size: 3rem }
.sm-h1 { font-size: 2rem }
.sm-h2 { font-size: 1.5rem }
.sm-h3 { font-size: 1.25rem }
.sm-h4 { font-size: 1rem }
.sm-h5 { font-size: .875rem }
.sm-h6 { font-size: .75rem }
}
@media (min-width: 52em) {
.md-h00 { font-size: 4rem }
.md-h0 { font-size: 3rem }
.md-h1 { font-size: 2rem }
.md-h2 { font-size: 1.5rem }
.md-h3 { font-size: 1.25rem }
.md-h4 { font-size: 1rem }
.md-h5 { font-size: .875rem }
.md-h6 { font-size: .75rem }
}
@media (min-width: 64em) {
.lg-h00 { font-size: 4rem }
.lg-h0 { font-size: 3rem }
.lg-h1 { font-size: 2rem }
.lg-h2 { font-size: 1.5rem }
.lg-h3 { font-size: 1.25rem }
.lg-h4 { font-size: 1rem }
.lg-h5 { font-size: .875rem }
.lg-h6 { font-size: .75rem }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="" />
<meta property="og:type" content="website" />
<meta property="og:title" content="0x" />
<meta property="og:description" content="" />
<meta property="og:image" content="/images/og_image.png" />
<title>0x: The Protocol for Trading Tokens</title>
<link rel="icon" type="image/png" href="/images/favicon/favicon-2-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="/images/favicon/favicon-2-16x16.png" sizes="16x16" />
<link rel="stylesheet" href="/css/basscss_responsive_custom.css">
<link rel="stylesheet" href="/css/basscss_responsive_padding.css">
<link rel="stylesheet" href="/css/basscss_responsive_margin.css">
<link rel="stylesheet" href="/css/basscss_responsive_type_scale.css">
</head>
<body style="margin: 0px; min-width: 355px;">
<div id="app"></div>
<script type="text/javascript" crossorigin="anonymous" src="/bundle.js" charset="utf-8"></script>
</body>
</html>

View File

@ -0,0 +1,25 @@
import * as React from 'react';
import { Helmet } from 'react-helmet';
export interface MetaTagsProps {
title: string;
description: string;
imgSrc?: string;
}
export const MetaTags: React.StatelessComponent<MetaTagsProps> = ({ title, description, imgSrc }) => (
<Helmet>
<title>{title}</title>
<meta name="description" content={description} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:type" content="website" />
<meta property="og:image" content={imgSrc} />
<meta name="twitter:site" content="@0xproject" />
<meta name="twitter:image" content={imgSrc} />
</Helmet>
);
MetaTags.defaultProps = {
imgSrc: '/images/og_image.png',
};

View File

@ -0,0 +1,59 @@
import { darken, saturate } from 'polished';
import * as React from 'react';
import styled from 'styled-components';
/**
* AN EXAMPLE OF HOW TO CREATE A STYLED COMPONENT USING STYLED-COMPONENTS
* SEE: https://www.styled-components.com/docs/basics#coming-from-css
*/
export interface ButtonProps {
backgroundColor?: string;
borderColor?: string;
width?: string;
padding?: string;
type?: string;
isDisabled?: boolean;
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
className?: string;
}
const PlainButton: React.StatelessComponent<ButtonProps> = ({ children, isDisabled, onClick, type, className }) => (
<button type={type} className={className} onClick={isDisabled ? undefined : onClick} disabled={isDisabled}>
{children}
</button>
);
const darkenOnHoverAmount = 0.1;
const darkenOnActiveAmount = 0.2;
const saturateOnFocusAmount = 0.2;
export const Button = styled(PlainButton)`
cursor: ${props => (props.isDisabled ? 'default' : 'pointer')};
transition: background-color, opacity 0.5s ease;
padding: ${props => props.padding};
border-radius: 3px;
outline: none;
width: ${props => props.width};
background-color: ${props => (props.backgroundColor ? props.backgroundColor : 'none')};
border: ${props => (props.borderColor ? `1px solid ${props.backgroundColor}` : 'none')};
&:hover {
background-color: ${props =>
!props.isDisabled ? darken(darkenOnHoverAmount, props.backgroundColor) : ''} !important;
}
&:active {
background-color: ${props => (!props.isDisabled ? darken(darkenOnActiveAmount, props.backgroundColor) : '')};
}
&:disabled {
opacity: 0.5;
}
&:focus {
background-color: ${props => saturate(saturateOnFocusAmount, props.backgroundColor)};
}
`;
Button.defaultProps = {
backgroundColor: 'red',
width: 'auto',
isDisabled: false,
padding: '1em 2.2em',
};
Button.displayName = 'Button';

View File

@ -0,0 +1,55 @@
import * as React from 'react';
type StringOrNum = string | number;
export type ContainerTag = 'div' | 'span';
export interface ContainerProps {
marginTop?: StringOrNum;
marginBottom?: StringOrNum;
marginRight?: StringOrNum;
marginLeft?: StringOrNum;
padding?: StringOrNum;
paddingTop?: StringOrNum;
paddingBottom?: StringOrNum;
paddingRight?: StringOrNum;
paddingLeft?: StringOrNum;
backgroundColor?: string;
borderRadius?: StringOrNum;
maxWidth?: StringOrNum;
maxHeight?: StringOrNum;
width?: StringOrNum;
height?: StringOrNum;
minWidth?: StringOrNum;
minHeight?: StringOrNum;
isHidden?: boolean;
className?: string;
position?: 'absolute' | 'fixed' | 'relative' | 'unset';
display?: 'inline-block' | 'block' | 'inline-flex' | 'inline';
top?: string;
left?: string;
right?: string;
bottom?: string;
zIndex?: number;
Tag?: ContainerTag;
cursor?: string;
id?: string;
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
overflowX?: 'scroll' | 'hidden' | 'auto' | 'visible';
}
export const Container: React.StatelessComponent<ContainerProps> = props => {
const { children, className, Tag, isHidden, id, onClick, ...style } = props;
const visibility = isHidden ? 'hidden' : undefined;
return (
<Tag id={id} style={{ ...style, visibility }} className={className} onClick={onClick}>
{children}
</Tag>
);
};
Container.defaultProps = {
Tag: 'div',
};
Container.displayName = 'Container';

View File

@ -0,0 +1,74 @@
import { colors } from '@0xproject/react-shared';
import { darken } from 'polished';
import * as React from 'react';
import styled from 'styled-components';
export type TextTag = 'p' | 'div' | 'span' | 'label' | 'h1' | 'h2' | 'h3' | 'h4' | 'i';
export interface TextProps {
className?: string;
Tag?: TextTag;
fontSize?: string;
fontFamily?: string;
fontStyle?: string;
fontColor?: string;
lineHeight?: string;
minHeight?: string;
center?: boolean;
fontWeight?: number | string;
textDecorationLine?: string;
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
hoverColor?: string;
noWrap?: boolean;
display?: string;
}
const PlainText: React.StatelessComponent<TextProps> = ({ children, className, onClick, Tag }) => (
<Tag className={className} onClick={onClick}>
{children}
</Tag>
);
export const Text = styled(PlainText)`
font-family: ${props => props.fontFamily};
font-style: ${props => props.fontStyle};
font-weight: ${props => props.fontWeight};
font-size: ${props => props.fontSize};
text-decoration-line: ${props => props.textDecorationLine};
${props => (props.lineHeight ? `line-height: ${props.lineHeight}` : '')};
${props => (props.center ? 'text-align: center' : '')};
color: ${props => props.fontColor};
${props => (props.minHeight ? `min-height: ${props.minHeight}` : '')};
${props => (props.onClick ? 'cursor: pointer' : '')};
transition: color 0.5s ease;
${props => (props.noWrap ? 'white-space: nowrap' : '')};
${props => (props.display ? `display: ${props.display}` : '')};
&:hover {
${props => (props.onClick ? `color: ${props.hoverColor || darken(0.3, props.fontColor || 'black')}` : '')};
}
`;
Text.defaultProps = {
fontFamily: 'Roboto',
fontStyle: 'normal',
fontWeight: 400,
fontColor: colors.black,
fontSize: '15px',
lineHeight: '1.5em',
textDecorationLine: 'none',
Tag: 'div',
noWrap: false,
};
Text.displayName = 'Text';
export const Title: React.StatelessComponent<TextProps> = props => <Text {...props} />;
Title.defaultProps = {
Tag: 'h2',
fontSize: '20px',
fontWeight: 600,
fontColor: colors.black,
};
Title.displayName = 'Title';

View File

@ -0,0 +1,9 @@
declare module 'whatwg-fetch';
declare module 'react-document-title';
declare module '*.json' {
const json: any;
/* tslint:disable */
export default json;
/* tslint:enable */
}

View File

@ -0,0 +1,17 @@
import * as React from 'react';
import { render } from 'react-dom';
import { MetaTags } from 'ts/components/meta_tags';
import { Landing } from 'ts/pages/landing';
import 'basscss/css/basscss.css';
const DOCUMENT_TITLE = '';
const DOCUMENT_DESCRIPTION = '';
render(
<div>
<MetaTags title={DOCUMENT_TITLE} description={DOCUMENT_DESCRIPTION} />
<Landing />
</div>,
document.getElementById('app'),
);

View File

@ -0,0 +1,27 @@
import * as _ from 'lodash';
import * as React from 'react';
import { Button } from '../components/ui/button';
import { Container } from '../components/ui/container';
import { Text } from '../components/ui/text';
interface LandingProps {}
interface LandingState {}
export class Landing extends React.Component<LandingProps, LandingState> {
constructor(props: LandingProps) {
super(props);
}
public render(): React.ReactNode {
return (
<Container id="landing" className="clearfix">
<Container className="mx-auto p4" width="200px">
<Button>
<Text fontColor="white">Click me!</Text>
</Button>
</Container>
</Container>
);
}
}

View File

@ -0,0 +1,32 @@
import * as bowser from 'bowser';
import * as _ from 'lodash';
export const utils = {
getColSize(items: number): number {
const bassCssGridSize = 12; // Source: http://basscss.com/#basscss-grid
const colSize = bassCssGridSize / items;
if (!_.isInteger(colSize)) {
throw new Error(`Number of cols must be divisible by ${bassCssGridSize}`);
}
return colSize;
},
getCurrentBaseUrl(): string {
const port = window.location.port;
const hasPort = !_.isUndefined(port);
const baseUrl = `https://${window.location.hostname}${hasPort ? `:${port}` : ''}`;
return baseUrl;
},
onPageLoadPromise: new Promise<void>((resolve, _reject) => {
if (document.readyState === 'complete') {
resolve();
return;
}
window.onload = () => resolve();
}),
openUrl(url: string): void {
window.open(url, '_blank');
},
isMobileOperatingSystem(): boolean {
return bowser.mobile;
},
};

View File

@ -0,0 +1,22 @@
{
"extends": "../../tsconfig",
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"outDir": "./transpiled/",
"jsx": "react",
"baseUrl": "./",
"allowJs": true,
"strictNullChecks": false,
"noImplicitThis": false,
// tsconfig.json at the monorepo root contains some options required for
// project references which do not work for website. We override those
// options here.
"declaration": false,
"declarationMap": false,
"composite": false,
"paths": {
"*": ["node_modules/@types/*", "*"]
}
},
"include": ["./ts/**/*"]
}

View File

@ -0,0 +1,10 @@
{
"extends": ["@0xproject/tslint-config"],
"rules": {
"no-implicit-dependencies": false,
"no-object-literal-type-assertion": false,
"completed-docs": false,
"prefer-function-over-method": false,
"custom-no-magic-numbers": false
}
}

View File

@ -0,0 +1,86 @@
const path = require('path');
const webpack = require('webpack');
const TerserPlugin = require('terser-webpack-plugin');
const childProcess = require('child_process');
const config = {
entry: ['./ts/index.tsx'],
output: {
path: path.join(__dirname, '/public'),
filename: 'bundle.js',
chunkFilename: 'bundle-[name].js',
publicPath: '/',
},
devtool: 'source-map',
resolve: {
modules: [path.join(__dirname, '/ts'), 'node_modules'],
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
alias: {
ts: path.join(__dirname, '/ts'),
less: path.join(__dirname, '/less'),
},
},
module: {
rules: [
{
test: /\.js$/,
loader: 'source-map-loader',
exclude: [
// instead of /\/node_modules\//
path.join(process.cwd(), 'node_modules'),
path.join(process.cwd(), '../..', 'node_modules'),
],
},
{
test: /\.tsx?$/,
loader: 'awesome-typescript-loader',
},
{
test: /\.md$/,
use: 'raw-loader',
},
{
test: /\.less$/,
loader: 'style-loader!css-loader!less-loader',
exclude: /node_modules/,
},
{
test: /\.css$/,
loaders: ['style-loader', 'css-loader'],
},
],
},
optimization: {
minimizer: [
new TerserPlugin({
sourceMap: true,
}),
],
},
devServer: {
port: 3572,
disableHostCheck: true,
},
};
module.exports = (_env, argv) => {
let plugins = [];
if (argv.mode === 'development') {
config.mode = 'development';
} else {
config.mode = 'production';
plugins = plugins.concat([
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV),
},
}),
]);
}
console.log('i 「atl」: Mode: ', config.mode);
config.plugins = plugins;
console.log('i 「atl」: Plugin Count: ', config.plugins.length);
return config;
};

View File

@ -1,4 +1,13 @@
[ [
{
"version": "1.1.0",
"changes": [
{
"note": "Add `JSONRPCResponseError` and error field on `JSONRPCResponsePayload`.",
"pr": 1102
}
]
},
{ {
"timestamp": 1538693146, "timestamp": 1538693146,
"version": "1.0.11", "version": "1.0.11",

View File

@ -113,10 +113,16 @@ export interface JSONRPCRequestPayload {
jsonrpc: string; jsonrpc: string;
} }
export interface JSONRPCResponseError {
message: string;
code: number;
}
export interface JSONRPCResponsePayload { export interface JSONRPCResponsePayload {
result: any; result: any;
id: number; id: number;
jsonrpc: string; jsonrpc: string;
error?: JSONRPCResponseError;
} }
export interface AbstractBlock { export interface AbstractBlock {

View File

@ -45,7 +45,7 @@
"@0xproject/utils": "^2.0.2", "@0xproject/utils": "^2.0.2",
"@0xproject/web3-wrapper": "^3.0.3", "@0xproject/web3-wrapper": "^3.0.3",
"ethereum-types": "^1.0.11", "ethereum-types": "^1.0.11",
"ethers": "4.0.0-beta.14", "ethers": "~4.0.4",
"lodash": "^4.17.5" "lodash": "^4.17.5"
}, },
"publishConfig": { "publishConfig": {

View File

@ -43,15 +43,19 @@
}, },
"homepage": "https://github.com/0xProject/0x-monorepo/packages/instant/README.md", "homepage": "https://github.com/0xProject/0x-monorepo/packages/instant/README.md",
"dependencies": { "dependencies": {
"@0xproject/connect": "^2.0.4", "@0xproject/asset-buyer": "^2.0.0",
"@0xproject/types": "^1.1.4", "@0xproject/types": "^1.1.4",
"@0xproject/typescript-typings": "^2.0.2", "@0xproject/typescript-typings": "^2.0.2",
"@0xproject/utils": "^1.0.11", "@0xproject/utils": "^2.0.2",
"@0xproject/web3-wrapper": "^3.0.3", "@0xproject/web3-wrapper": "^3.0.3",
"ethereum-types": "^1.0.11", "ethereum-types": "^1.0.11",
"lodash": "^4.17.10", "lodash": "^4.17.10",
"polished": "^2.2.0",
"react": "^16.5.2", "react": "^16.5.2",
"react-dom": "^16.5.2" "react-dom": "^16.5.2",
"react-redux": "^5.0.7",
"redux": "^4.0.0",
"styled-components": "^3.4.9"
}, },
"devDependencies": { "devDependencies": {
"@0xproject/tslint-config": "^1.0.8", "@0xproject/tslint-config": "^1.0.8",
@ -59,8 +63,10 @@
"@types/enzyme-adapter-react-16": "^1.0.3", "@types/enzyme-adapter-react-16": "^1.0.3",
"@types/lodash": "^4.14.116", "@types/lodash": "^4.14.116",
"@types/node": "*", "@types/node": "*",
"@types/react": "16.4.7", "@types/react": "^16.4.16",
"@types/react-dom": "^16.0.8", "@types/react-dom": "^16.0.8",
"@types/react-redux": "^6.0.9",
"@types/redux": "^3.6.0",
"awesome-typescript-loader": "^5.2.1", "awesome-typescript-loader": "^5.2.1",
"copyfiles": "^1.2.0", "copyfiles": "^1.2.0",
"enzyme": "^3.6.0", "enzyme": "^3.6.0",

View File

@ -6,6 +6,19 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>0x Instant Dev Environment</title> <title>0x Instant Dev Environment</title>
<script type="text/javascript" src="/main.bundle.js" charset="utf-8"></script> <script type="text/javascript" src="/main.bundle.js" charset="utf-8"></script>
<style>
#zeroExInstantContainer {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
body {
margin: 0;
background-color: rgba(0, 0, 0, 0.2);
}
</style>
</head> </head>
<body> <body>

View File

@ -0,0 +1,47 @@
import { BigNumber } from '@0xproject/utils';
import * as _ from 'lodash';
import * as React from 'react';
import { ColorOption } from '../style/theme';
import { Container, Input } from './ui';
export interface AmountInputProps {
fontColor?: ColorOption;
fontSize?: string;
value?: BigNumber;
onChange?: (value?: BigNumber) => void;
}
export class AmountInput extends React.Component<AmountInputProps> {
public render(): React.ReactNode {
const { fontColor, fontSize, value } = this.props;
return (
<Container borderBottom="1px solid rgba(255,255,255,0.3)" display="inline-block">
<Input
fontColor={fontColor}
fontSize={fontSize}
onChange={this._handleChange}
value={!_.isUndefined(value) ? value.toString() : ''}
placeholder="0.00"
width="2em"
/>
</Container>
);
}
private readonly _handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
const value = event.target.value;
let bigNumberValue;
if (!_.isEmpty(value)) {
try {
bigNumberValue = new BigNumber(event.target.value);
} catch {
// We don't want to allow values that can't be a BigNumber, so don't even call onChange.
return;
}
}
if (!_.isUndefined(this.props.onChange)) {
this.props.onChange(bigNumberValue);
}
};
}

View File

@ -0,0 +1,19 @@
import * as React from 'react';
import { ColorOption } from '../style/theme';
import { Button, Container, Text } from './ui';
export interface BuyButtonProps {}
export const BuyButton: React.StatelessComponent<BuyButtonProps> = props => (
<Container padding="20px" width="100%">
<Button width="100%">
<Text fontColor={ColorOption.white} fontWeight={600} fontSize="20px">
Buy
</Text>
</Button>
</Container>
);
BuyButton.displayName = 'BuyButton';

View File

@ -0,0 +1,45 @@
import * as React from 'react';
import { SelectedAssetAmountInput } from '../containers/selected_asset_amount_input';
import { ColorOption } from '../style/theme';
import { Container, Flex, Text } from './ui';
export interface InstantHeadingProps {}
export const InstantHeading: React.StatelessComponent<InstantHeadingProps> = props => (
<Container backgroundColor={ColorOption.primaryColor} padding="20px" width="100%" borderRadius="3px 3px 0px 0px">
<Container marginBottom="5px">
<Text
letterSpacing="1px"
fontColor={ColorOption.white}
opacity={0.7}
fontWeight={500}
textTransform="uppercase"
fontSize="12px"
>
I want to buy
</Text>
</Container>
<Flex direction="row" justify="space-between">
<Container>
<SelectedAssetAmountInput fontSize="45px" />
<Container display="inline-block" marginLeft="10px">
<Text fontSize="45px" fontColor={ColorOption.white} textTransform="uppercase">
rep
</Text>
</Container>
</Container>
<Flex direction="column" justify="space-between">
<Container marginBottom="5px">
<Text fontSize="16px" fontColor={ColorOption.white} fontWeight={500}>
0 ETH
</Text>
</Container>
<Text fontSize="16px" fontColor={ColorOption.white} opacity={0.7}>
$0.00
</Text>
</Flex>
</Flex>
</Container>
);

View File

@ -0,0 +1,62 @@
import * as React from 'react';
import { ColorOption } from '../style/theme';
import { Container, Flex, Text } from './ui';
export interface OrderDetailsProps {}
export const OrderDetails: React.StatelessComponent<OrderDetailsProps> = props => (
<Container padding="20px" width="100%">
<Container marginBottom="10px">
<Text
letterSpacing="1px"
fontColor={ColorOption.primaryColor}
fontWeight={600}
textTransform="uppercase"
fontSize="14px"
>
Order Details
</Text>
</Container>
<OrderDetailsRow name="Token Price" primaryValue=".013 ETH" secondaryValue="$24.32" />
<OrderDetailsRow name="Fee" primaryValue=".005 ETH" secondaryValue="$1.04" />
<OrderDetailsRow name="Total Cost" primaryValue="1.66 ETH" secondaryValue="$589.56" shouldEmphasize={true} />
</Container>
);
OrderDetails.displayName = 'OrderDetails';
export interface OrderDetailsRowProps {
name: string;
primaryValue: string;
secondaryValue: string;
shouldEmphasize?: boolean;
}
export const OrderDetailsRow: React.StatelessComponent<OrderDetailsRowProps> = props => {
const fontWeight = props.shouldEmphasize ? 700 : 400;
return (
<Container padding="10px 0px" borderTop="1px dashed" borderColor={ColorOption.feintGrey}>
<Flex justify="space-between">
<Text fontWeight={fontWeight} fontColor={ColorOption.grey}>
{props.name}
</Text>
<Container>
<Container marginRight="3px" display="inline-block">
<Text fontColor={ColorOption.lightGrey}>({props.secondaryValue}) </Text>
</Container>
<Text fontWeight={fontWeight} fontColor={ColorOption.grey}>
{props.primaryValue}
</Text>
</Container>
</Flex>
</Container>
);
};
OrderDetailsRow.defaultProps = {
shouldEmphasize: false,
};
OrderDetailsRow.displayName = 'OrderDetailsRow';

View File

@ -0,0 +1,60 @@
import { darken, saturate } from 'polished';
import * as React from 'react';
import { ColorOption, styled } from '../../style/theme';
export interface ButtonProps {
backgroundColor?: ColorOption;
borderColor?: ColorOption;
width?: string;
padding?: string;
type?: string;
isDisabled?: boolean;
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
className?: string;
}
const PlainButton: React.StatelessComponent<ButtonProps> = ({ children, isDisabled, onClick, type, className }) => (
<button type={type} className={className} onClick={isDisabled ? undefined : onClick} disabled={isDisabled}>
{children}
</button>
);
const darkenOnHoverAmount = 0.1;
const darkenOnActiveAmount = 0.2;
const saturateOnFocusAmount = 0.2;
export const Button = styled(PlainButton)`
cursor: ${props => (props.isDisabled ? 'default' : 'pointer')};
transition: background-color, opacity 0.5s ease;
padding: ${props => props.padding};
border-radius: 3px;
outline: none;
width: ${props => props.width};
background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')};
border: ${props => (props.borderColor ? `1px solid ${props.theme[props.borderColor]}` : 'none')};
&:hover {
background-color: ${props =>
!props.isDisabled
? darken(darkenOnHoverAmount, props.theme[props.backgroundColor || 'white'])
: ''} !important;
}
&:active {
background-color: ${props =>
!props.isDisabled ? darken(darkenOnActiveAmount, props.theme[props.backgroundColor || 'white']) : ''};
}
&:disabled {
opacity: 0.5;
}
&:focus {
background-color: ${props => saturate(saturateOnFocusAmount, props.theme[props.backgroundColor || 'white'])};
}
`;
Button.defaultProps = {
backgroundColor: ColorOption.primaryColor,
width: 'auto',
isDisabled: false,
padding: '1em 2.2em',
};
Button.displayName = 'Button';

View File

@ -0,0 +1,64 @@
import * as React from 'react';
import { ColorOption, styled } from '../../style/theme';
import { cssRuleIfExists } from '../../style/util';
export interface ContainerProps {
display?: string;
position?: string;
top?: string;
right?: string;
bottom?: string;
left?: string;
width?: string;
maxWidth?: string;
margin?: string;
marginTop?: string;
marginRight?: string;
marginBottom?: string;
marginLeft?: string;
padding?: string;
borderRadius?: string;
border?: string;
borderColor?: ColorOption;
borderTop?: string;
borderBottom?: string;
className?: string;
backgroundColor?: ColorOption;
hasBoxShadow?: boolean;
}
const PlainContainer: React.StatelessComponent<ContainerProps> = ({ children, className }) => (
<div className={className}>{children}</div>
);
export const Container = styled(PlainContainer)`
box-sizing: border-box;
${props => cssRuleIfExists(props, 'display')}
${props => cssRuleIfExists(props, 'position')}
${props => cssRuleIfExists(props, 'top')}
${props => cssRuleIfExists(props, 'right')}
${props => cssRuleIfExists(props, 'bottom')}
${props => cssRuleIfExists(props, 'left')}
${props => cssRuleIfExists(props, 'width')}
${props => cssRuleIfExists(props, 'max-width')}
${props => cssRuleIfExists(props, 'margin')}
${props => cssRuleIfExists(props, 'margin-top')}
${props => cssRuleIfExists(props, 'margin-right')}
${props => cssRuleIfExists(props, 'margin-bottom')}
${props => cssRuleIfExists(props, 'margin-left')}
${props => cssRuleIfExists(props, 'padding')}
${props => cssRuleIfExists(props, 'border-radius')}
${props => cssRuleIfExists(props, 'border')}
${props => cssRuleIfExists(props, 'border-top')}
${props => cssRuleIfExists(props, 'border-bottom')}
${props => (props.hasBoxShadow ? `box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1)` : '')};
background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')};
border-color: ${props => (props.borderColor ? props.theme[props.borderColor] : 'none')};
`;
Container.defaultProps = {
display: 'block',
};
Container.displayName = 'Container';

View File

@ -0,0 +1,37 @@
import * as React from 'react';
import { ColorOption, styled } from '../../style/theme';
import { cssRuleIfExists } from '../../style/util';
export interface FlexProps {
direction?: 'row' | 'column';
flexWrap?: 'wrap' | 'nowrap';
justify?: 'flex-start' | 'center' | 'space-around' | 'space-between' | 'space-evenly' | 'flex-end';
align?: 'flex-start' | 'center' | 'space-around' | 'space-between' | 'space-evenly' | 'flex-end';
width?: string;
backgroundColor?: ColorOption;
className?: string;
}
const PlainFlex: React.StatelessComponent<FlexProps> = ({ children, className }) => (
<div className={className}>{children}</div>
);
export const Flex = styled(PlainFlex)`
display: flex;
flex-direction: ${props => props.direction};
flex-wrap: ${props => props.flexWrap};
justify-content: ${props => props.justify};
align-items: ${props => props.align};
${props => cssRuleIfExists(props, 'width')}
background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')};
`;
Flex.defaultProps = {
direction: 'row',
flexWrap: 'nowrap',
justify: 'center',
align: 'center',
};
Flex.displayName = 'Flex';

View File

@ -0,0 +1,5 @@
export { Text, Title } from './text';
export { Button } from './button';
export { Flex } from './flex';
export { Container } from './container';
export { Input } from './input';

View File

@ -0,0 +1,40 @@
import * as React from 'react';
import { ColorOption, styled } from '../../style/theme';
export interface InputProps {
className?: string;
value?: string;
width?: string;
fontSize?: string;
fontColor?: ColorOption;
placeholder?: string;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
}
const PlainInput: React.StatelessComponent<InputProps> = ({ value, className, placeholder, onChange }) => (
<input className={className} value={value} onChange={onChange} placeholder={placeholder} />
);
export const Input = styled(PlainInput)`
font-size: ${props => props.fontSize};
width: ${props => props.width};
padding: 0.1em 0em;
font-family: 'Inter UI';
color: ${props => props.theme[props.fontColor || 'white']};
background: transparent;
outline: none;
border: none;
&::placeholder {
color: ${props => props.theme[props.fontColor || 'white']};
opacity: 0.5;
}
`;
Input.defaultProps = {
width: 'auto',
fontColor: ColorOption.white,
fontSize: '12px',
};
Input.displayName = 'Input';

View File

@ -0,0 +1,80 @@
import { darken } from 'polished';
import * as React from 'react';
import { ColorOption, styled } from '../../style/theme';
export interface TextProps {
fontColor?: ColorOption;
fontFamily?: string;
fontStyle?: string;
fontSize?: string;
opacity?: number;
letterSpacing?: string;
textTransform?: string;
lineHeight?: string;
className?: string;
minHeight?: string;
center?: boolean;
fontWeight?: number | string;
textDecorationLine?: string;
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
hoverColor?: string;
noWrap?: boolean;
display?: string;
}
const PlainText: React.StatelessComponent<TextProps> = ({ children, className, onClick }) => (
<div className={className} onClick={onClick}>
{children}
</div>
);
const darkenOnHoverAmount = 0.3;
export const Text = styled(PlainText)`
font-family: ${props => props.fontFamily};
font-style: ${props => props.fontStyle};
font-weight: ${props => props.fontWeight};
font-size: ${props => props.fontSize};
opacity: ${props => props.opacity};
text-decoration-line: ${props => props.textDecorationLine};
${props => (props.lineHeight ? `line-height: ${props.lineHeight}` : '')};
${props => (props.center ? 'text-align: center' : '')};
color: ${props => props.fontColor && props.theme[props.fontColor]};
${props => (props.minHeight ? `min-height: ${props.minHeight}` : '')};
${props => (props.onClick ? 'cursor: pointer' : '')};
transition: color 0.5s ease;
${props => (props.noWrap ? 'white-space: nowrap' : '')};
${props => (props.display ? `display: ${props.display}` : '')};
${props => (props.letterSpacing ? `letter-spacing: ${props.letterSpacing}` : '')};
${props => (props.textTransform ? `text-transform: ${props.textTransform}` : '')};
&:hover {
${props =>
props.onClick
? `color: ${props.hoverColor || darken(darkenOnHoverAmount, props.theme[props.fontColor || 'white'])}`
: ''};
}
`;
Text.defaultProps = {
fontFamily: 'Inter UI',
fontStyle: 'normal',
fontWeight: 400,
fontColor: ColorOption.black,
fontSize: '15px',
textDecorationLine: 'none',
noWrap: false,
display: 'inline-block',
};
Text.displayName = 'Text';
export const Title: React.StatelessComponent<TextProps> = props => <Text {...props} />;
Title.defaultProps = {
fontSize: '20px',
fontWeight: 600,
opacity: 1,
fontColor: ColorOption.primaryColor,
};
Title.displayName = 'Title';

View File

@ -1,5 +1,20 @@
import * as React from 'react'; import * as React from 'react';
import { Provider } from 'react-redux';
import { store } from '../redux/store';
import { fonts } from '../style/fonts';
import { theme, ThemeProvider } from '../style/theme';
import { ZeroExInstantContainer } from './zero_ex_instant_container';
fonts.include();
export interface ZeroExInstantProps {} export interface ZeroExInstantProps {}
export const ZeroExInstant: React.StatelessComponent<ZeroExInstantProps> = () => <div>ZeroExInstant</div>; export const ZeroExInstant: React.StatelessComponent<ZeroExInstantProps> = () => (
<Provider store={store}>
<ThemeProvider theme={theme}>
<ZeroExInstantContainer />
</ThemeProvider>
</Provider>
);

View File

@ -0,0 +1,20 @@
import * as React from 'react';
import { ColorOption } from '../style/theme';
import { BuyButton } from './buy_button';
import { InstantHeading } from './instant_heading';
import { OrderDetails } from './order_details';
import { Container, Flex } from './ui';
export interface ZeroExInstantContainerProps {}
export const ZeroExInstantContainer: React.StatelessComponent<ZeroExInstantContainerProps> = props => (
<Container hasBoxShadow={true} width="350px" backgroundColor={ColorOption.white} borderRadius="3px">
<Flex direction="column" justify="flex-start">
<InstantHeading />
<OrderDetails />
<BuyButton />
</Flex>
</Container>
);

View File

@ -0,0 +1,36 @@
import { BigNumber } from '@0xproject/utils';
import * as React from 'react';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import { State } from '../redux/reducer';
import { ColorOption } from '../style/theme';
import { Action, ActionTypes } from '../types';
import { AmountInput } from '../components/amount_input';
export interface SelectedAssetAmountInputProps {
fontColor?: ColorOption;
fontSize?: string;
}
interface ConnectedState {
value?: BigNumber;
}
interface ConnectedDispatch {
onChange?: (value?: BigNumber) => void;
}
const mapStateToProps = (state: State, _ownProps: SelectedAssetAmountInputProps): ConnectedState => ({
value: state.selectedAssetAmount,
});
const mapDispatchToProps = (dispatch: Dispatch<Action>): ConnectedDispatch => ({
onChange: value => dispatch({ type: ActionTypes.UPDATE_SELECTED_ASSET_AMOUNT, data: value }),
});
export const SelectedAssetAmountInput: React.ComponentClass<SelectedAssetAmountInputProps> = connect(
mapStateToProps,
mapDispatchToProps,
)(AmountInput);

View File

@ -0,0 +1,31 @@
import { BigNumber } from '@0xproject/utils';
import * as _ from 'lodash';
import { Action, ActionTypes } from '../types';
export interface State {
ethUsdPrice?: string;
selectedAssetAmount?: BigNumber;
}
export const INITIAL_STATE: State = {
ethUsdPrice: undefined,
selectedAssetAmount: undefined,
};
export const reducer = (state: State = INITIAL_STATE, action: Action): State => {
switch (action.type) {
case ActionTypes.UPDATE_ETH_USD_PRICE:
return {
...state,
ethUsdPrice: action.data,
};
case ActionTypes.UPDATE_SELECTED_ASSET_AMOUNT:
return {
...state,
selectedAssetAmount: action.data,
};
default:
return state;
}
};

View File

@ -0,0 +1,6 @@
import * as _ from 'lodash';
import { createStore, Store as ReduxStore } from 'redux';
import { reducer, State } from './reducer';
export const store: ReduxStore<State> = createStore(reducer);

View File

@ -0,0 +1,10 @@
import { injectGlobal } from './theme';
export const fonts = {
include: () => {
// Inject the inter-ui font into the page
return injectGlobal`
@import url('https://rsms.me/inter/inter-ui.css');
`;
},
};

View File

@ -0,0 +1,27 @@
import * as styledComponents from 'styled-components';
const { default: styled, css, injectGlobal, keyframes, ThemeProvider } = styledComponents;
export type Theme = { [key in ColorOption]: string };
export enum ColorOption {
primaryColor = 'primaryColor',
black = 'black',
lightGrey = 'lightGrey',
grey = 'grey',
feintGrey = 'feintGrey',
darkGrey = 'darkGrey',
white = 'white',
}
export const theme: Theme = {
primaryColor: '#512D80',
black: 'black',
lightGrey: '#999999',
grey: '#666666',
feintGrey: '#DEDEDE',
darkGrey: '#333333',
white: 'white',
};
export { styled, css, injectGlobal, keyframes, ThemeProvider };

View File

@ -0,0 +1,11 @@
import { ObjectMap } from '@0xproject/types';
import * as _ from 'lodash';
export const cssRuleIfExists = (props: ObjectMap<any>, rule: string): string => {
const camelCaseRule = _.camelCase(rule);
const ruleValueIfExists = props[camelCaseRule];
if (!_.isUndefined(ruleValueIfExists)) {
return `${rule}: ${ruleValueIfExists};`;
}
return '';
};

View File

@ -0,0 +1,9 @@
export enum ActionTypes {
UPDATE_ETH_USD_PRICE,
UPDATE_SELECTED_ASSET_AMOUNT,
}
export interface Action {
type: ActionTypes;
data?: any;
}

View File

@ -1,3 +1,7 @@
{ {
"extends": ["@0xproject/tslint-config"] "extends": ["@0xproject/tslint-config"],
"rules": {
"custom-no-magic-numbers": false,
"semicolon": [true, "always", "ignore-bound-class-methods"]
}
} }

View File

@ -0,0 +1,28 @@
export const eip712TypedDataSchema = {
id: '/eip712TypedData',
type: 'object',
properties: {
types: {
type: 'object',
properties: {
EIP712Domain: { type: 'array' },
},
additionalProperties: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
type: { type: 'string' },
},
required: ['name', 'type'],
},
},
required: ['EIP712Domain'],
},
primaryType: { type: 'string' },
domain: { type: 'object' },
message: { type: 'object' },
},
required: ['types', 'primaryType', 'domain', 'message'],
};

View File

@ -0,0 +1,10 @@
export const zeroExTransactionSchema = {
id: '/zeroExTransactionSchema',
properties: {
data: { $ref: '/hexSchema' },
signerAddress: { $ref: '/addressSchema' },
salt: { $ref: '/numberSchema' },
},
required: ['data', 'salt', 'signerAddress'],
type: 'object',
};

View File

@ -2,6 +2,7 @@ import { addressSchema, hexSchema, numberSchema } from '../schemas/basic_type_sc
import { blockParamSchema, blockRangeSchema } from '../schemas/block_range_schema'; import { blockParamSchema, blockRangeSchema } from '../schemas/block_range_schema';
import { callDataSchema } from '../schemas/call_data_schema'; import { callDataSchema } from '../schemas/call_data_schema';
import { ecSignatureParameterSchema, ecSignatureSchema } from '../schemas/ec_signature_schema'; import { ecSignatureParameterSchema, ecSignatureSchema } from '../schemas/ec_signature_schema';
import { eip712TypedDataSchema } from '../schemas/eip712_typed_data';
import { indexFilterValuesSchema } from '../schemas/index_filter_values_schema'; import { indexFilterValuesSchema } from '../schemas/index_filter_values_schema';
import { orderCancellationRequestsSchema } from '../schemas/order_cancel_schema'; import { orderCancellationRequestsSchema } from '../schemas/order_cancel_schema';
import { orderFillOrKillRequestsSchema } from '../schemas/order_fill_or_kill_requests_schema'; import { orderFillOrKillRequestsSchema } from '../schemas/order_fill_or_kill_requests_schema';
@ -31,6 +32,7 @@ import { relayerApiOrdersSchema } from '../schemas/relayer_api_orders_schema';
import { signedOrdersSchema } from '../schemas/signed_orders_schema'; import { signedOrdersSchema } from '../schemas/signed_orders_schema';
import { tokenSchema } from '../schemas/token_schema'; import { tokenSchema } from '../schemas/token_schema';
import { jsNumber, txDataSchema } from '../schemas/tx_data_schema'; import { jsNumber, txDataSchema } from '../schemas/tx_data_schema';
import { zeroExTransactionSchema } from '../schemas/zero_ex_transaction_schema';
export const schemas = { export const schemas = {
numberSchema, numberSchema,
@ -39,6 +41,7 @@ export const schemas = {
hexSchema, hexSchema,
ecSignatureParameterSchema, ecSignatureParameterSchema,
ecSignatureSchema, ecSignatureSchema,
eip712TypedDataSchema,
indexFilterValuesSchema, indexFilterValuesSchema,
orderCancellationRequestsSchema, orderCancellationRequestsSchema,
orderFillOrKillRequestsSchema, orderFillOrKillRequestsSchema,
@ -68,4 +71,5 @@ export const schemas = {
relayerApiOrdersChannelUpdateSchema, relayerApiOrdersChannelUpdateSchema,
relayerApiOrdersResponseSchema, relayerApiOrdersResponseSchema,
relayerApiAssetDataPairsSchema, relayerApiAssetDataPairsSchema,
zeroExTransactionSchema,
}; };

View File

@ -41,7 +41,7 @@
"@types/mocha": "^5.2.2", "@types/mocha": "^5.2.2",
"copyfiles": "^2.0.0", "copyfiles": "^2.0.0",
"ethereum-types": "^1.0.11", "ethereum-types": "^1.0.11",
"ethers": "4.0.0-beta.14", "ethers": "~4.0.4",
"lodash": "^4.17.5", "lodash": "^4.17.5",
"run-s": "^0.0.0" "run-s": "^0.0.0"
}, },

View File

@ -54,7 +54,7 @@
"@0xproject/web3-wrapper": "^3.0.3", "@0xproject/web3-wrapper": "^3.0.3",
"@ledgerhq/hw-app-eth": "^4.3.0", "@ledgerhq/hw-app-eth": "^4.3.0",
"ethereum-types": "^1.0.11", "ethereum-types": "^1.0.11",
"ethers": "4.0.0-beta.14", "ethers": "~4.0.4",
"lodash": "^4.17.5" "lodash": "^4.17.5"
}, },
"optionalDependencies": { "optionalDependencies": {

View File

@ -1,4 +1,22 @@
[ [
{
"version": "2.0.0",
"changes": [
{
"note":
"Added `ecSignOrderAsync` to first sign an order using `eth_signTypedData` and fallback to `eth_sign`.",
"pr": 1102
},
{
"note": "Added `ecSignTypedDataOrderAsync` to sign an order exclusively using `eth_signTypedData`.",
"pr": 1102
},
{
"note": "Rename `ecSignOrderHashAsync` to `ecSignHashAsync` removing `SignerType` parameter.",
"pr": 1102
}
]
},
{ {
"version": "1.0.7", "version": "1.0.7",
"changes": [ "changes": [

View File

@ -70,7 +70,7 @@
"ethereum-types": "^1.0.11", "ethereum-types": "^1.0.11",
"ethereumjs-abi": "0.6.5", "ethereumjs-abi": "0.6.5",
"ethereumjs-util": "^5.1.1", "ethereumjs-util": "^5.1.1",
"ethers": "4.0.0-beta.14", "ethers": "~4.0.4",
"lodash": "^4.17.5" "lodash": "^4.17.5"
}, },
"publishConfig": { "publishConfig": {

View File

@ -13,4 +13,39 @@ export const constants = {
BASE_16: 16, BASE_16: 16,
INFINITE_TIMESTAMP_SEC: new BigNumber(2524604400), // Close to infinite INFINITE_TIMESTAMP_SEC: new BigNumber(2524604400), // Close to infinite
ZERO_AMOUNT: new BigNumber(0), ZERO_AMOUNT: new BigNumber(0),
EIP712_DOMAIN_NAME: '0x Protocol',
EIP712_DOMAIN_VERSION: '2',
EIP712_DOMAIN_SCHEMA: {
name: 'EIP712Domain',
parameters: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'verifyingContract', type: 'address' },
],
},
EIP712_ORDER_SCHEMA: {
name: 'Order',
parameters: [
{ name: 'makerAddress', type: 'address' },
{ name: 'takerAddress', type: 'address' },
{ name: 'feeRecipientAddress', type: 'address' },
{ name: 'senderAddress', type: 'address' },
{ name: 'makerAssetAmount', type: 'uint256' },
{ name: 'takerAssetAmount', type: 'uint256' },
{ name: 'makerFee', type: 'uint256' },
{ name: 'takerFee', type: 'uint256' },
{ name: 'expirationTimeSeconds', type: 'uint256' },
{ name: 'salt', type: 'uint256' },
{ name: 'makerAssetData', type: 'bytes' },
{ name: 'takerAssetData', type: 'bytes' },
],
},
EIP712_ZEROEX_TRANSACTION_SCHEMA: {
name: 'ZeroExTransaction',
parameters: [
{ name: 'salt', type: 'uint256' },
{ name: 'signerAddress', type: 'address' },
{ name: 'data', type: 'bytes' },
],
},
}; };

View File

@ -1,109 +1,83 @@
import ethUtil = require('ethereumjs-util'); import { assert } from '@0xproject/assert';
import { schemas } from '@0xproject/json-schemas';
import { EIP712Object, EIP712TypedData, EIP712Types, Order, ZeroExTransaction } from '@0xproject/types';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { crypto } from './crypto'; import { constants } from './constants';
import { EIP712Schema, EIP712Types } from './types';
const EIP191_PREFIX = '\x19\x01';
const EIP712_DOMAIN_NAME = '0x Protocol';
const EIP712_DOMAIN_VERSION = '2';
const EIP712_VALUE_LENGTH = 32;
const EIP712_DOMAIN_SCHEMA: EIP712Schema = {
name: 'EIP712Domain',
parameters: [
{ name: 'name', type: EIP712Types.String },
{ name: 'version', type: EIP712Types.String },
{ name: 'verifyingContract', type: EIP712Types.Address },
],
};
export const eip712Utils = { export const eip712Utils = {
/** /**
* Compiles the EIP712Schema and returns the hash of the schema. * Creates a EIP712TypedData object specific to the 0x protocol for use with signTypedData.
* @param schema The EIP712 schema. * @param primaryType The primary type found in message
* @return The hash of the compiled schema * @param types The additional types for the data in message
* @param message The contents of the message
* @param exchangeAddress The address of the exchange contract
* @return A typed data object
*/ */
compileSchema(schema: EIP712Schema): Buffer { createTypedData: (
const eip712Schema = eip712Utils._encodeType(schema); primaryType: string,
const eip712SchemaHashBuffer = crypto.solSHA3([eip712Schema]); types: EIP712Types,
return eip712SchemaHashBuffer; message: EIP712Object,
exchangeAddress: string,
): EIP712TypedData => {
assert.isETHAddressHex('exchangeAddress', exchangeAddress);
assert.isString('primaryType', primaryType);
const typedData = {
types: {
EIP712Domain: constants.EIP712_DOMAIN_SCHEMA.parameters,
...types,
},
domain: {
name: constants.EIP712_DOMAIN_NAME,
version: constants.EIP712_DOMAIN_VERSION,
verifyingContract: exchangeAddress,
},
message,
primaryType,
};
assert.doesConformToSchema('typedData', typedData, schemas.eip712TypedDataSchema);
return typedData;
}, },
/** /**
* Merges the EIP712 hash of a struct with the DomainSeparator for 0x v2. * Creates an Order EIP712TypedData object for use with signTypedData.
* @param hashStruct the EIP712 hash of a struct * @param Order the order
* @param contractAddress the exchange contract address * @return A typed data object
* @return The hash of an EIP712 message with domain separator prefixed
*/ */
createEIP712Message(hashStruct: Buffer, contractAddress: string): Buffer { createOrderTypedData: (order: Order): EIP712TypedData => {
const domainSeparatorHashBuffer = eip712Utils._getDomainSeparatorHashBuffer(contractAddress); assert.doesConformToSchema('order', order, schemas.orderSchema, [schemas.hexSchema]);
const messageBuff = crypto.solSHA3([EIP191_PREFIX, domainSeparatorHashBuffer, hashStruct]); const normalizedOrder = _.mapValues(order, value => {
return messageBuff; return !_.isString(value) ? value.toString() : value;
},
/**
* 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);
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);
},
_getDomainSeparatorHashBuffer(exchangeAddress: string): Buffer {
const domainSeparatorSchemaBuffer = eip712Utils._getDomainSeparatorSchemaBuffer();
const encodedData = eip712Utils._encodeData(EIP712_DOMAIN_SCHEMA, {
name: EIP712_DOMAIN_NAME,
version: EIP712_DOMAIN_VERSION,
verifyingContract: exchangeAddress,
}); });
const domainSeparatorHashBuff2 = crypto.solSHA3([domainSeparatorSchemaBuffer, ...encodedData]); const typedData = eip712Utils.createTypedData(
return domainSeparatorHashBuff2; constants.EIP712_ORDER_SCHEMA.name,
{ Order: constants.EIP712_ORDER_SCHEMA.parameters },
normalizedOrder,
order.exchangeAddress,
);
return typedData;
}, },
_encodeType(schema: EIP712Schema): string { /**
const namedTypes = _.map(schema.parameters, ({ name, type }) => `${type} ${name}`); * Creates an ExecuteTransaction EIP712TypedData object for use with signTypedData and
const namedTypesJoined = namedTypes.join(','); * 0x Exchange executeTransaction.
const encodedType = `${schema.name}(${namedTypesJoined})`; * @param ZeroExTransaction the 0x transaction
return encodedType; * @param exchangeAddress The address of the exchange contract
}, * @return A typed data object
_encodeData(schema: EIP712Schema, data: { [key: string]: any }): any { */
const encodedValues = []; createZeroExTransactionTypedData: (
for (const parameter of schema.parameters) { zeroExTransaction: ZeroExTransaction,
const value = data[parameter.name]; exchangeAddress: string,
if (parameter.type === EIP712Types.String || parameter.type === EIP712Types.Bytes) { ): EIP712TypedData => {
encodedValues.push(crypto.solSHA3([ethUtil.toBuffer(value)])); assert.isETHAddressHex('exchangeAddress', exchangeAddress);
} else if (parameter.type === EIP712Types.Uint256) { assert.doesConformToSchema('zeroExTransaction', zeroExTransaction, schemas.zeroExTransactionSchema);
encodedValues.push(value); const normalizedTransaction = _.mapValues(zeroExTransaction, value => {
} else if (parameter.type === EIP712Types.Address) { return !_.isString(value) ? value.toString() : value;
encodedValues.push(eip712Utils.pad32Address(value)); });
} else { const typedData = eip712Utils.createTypedData(
throw new Error(`Unable to encode ${parameter.type}`); constants.EIP712_ZEROEX_TRANSACTION_SCHEMA.name,
} { ZeroExTransaction: constants.EIP712_ZEROEX_TRANSACTION_SCHEMA.parameters },
} normalizedTransaction,
return encodedValues; exchangeAddress,
);
return typedData;
}, },
}; };

View File

@ -2,7 +2,6 @@ export { orderHashUtils } from './order_hash';
export { signatureUtils } from './signature_utils'; export { signatureUtils } from './signature_utils';
export { generatePseudoRandomSalt } from './salt'; export { generatePseudoRandomSalt } from './salt';
export { assetDataUtils } from './asset_data_utils'; export { assetDataUtils } from './asset_data_utils';
export { eip712Utils } from './eip712_utils';
export { marketUtils } from './market_utils'; export { marketUtils } from './market_utils';
export { rateUtils } from './rate_utils'; export { rateUtils } from './rate_utils';
export { sortingUtils } from './sorting_utils'; export { sortingUtils } from './sorting_utils';
@ -19,7 +18,17 @@ export { ExchangeTransferSimulator } from './exchange_transfer_simulator';
export { BalanceAndProxyAllowanceLazyStore } from './store/balance_and_proxy_allowance_lazy_store'; export { BalanceAndProxyAllowanceLazyStore } from './store/balance_and_proxy_allowance_lazy_store';
export { OrderFilledCancelledLazyStore } from './store/order_filled_cancelled_lazy_store'; export { OrderFilledCancelledLazyStore } from './store/order_filled_cancelled_lazy_store';
export { Provider, JSONRPCRequestPayload, JSONRPCErrorCallback, JSONRPCResponsePayload } from 'ethereum-types'; export { constants } from './constants';
export { eip712Utils } from './eip712_utils';
export {
Provider,
JSONRPCRequestPayload,
JSONRPCErrorCallback,
JSONRPCResponsePayload,
JSONRPCResponseError,
} from 'ethereum-types';
export { export {
SignedOrder, SignedOrder,
Order, Order,
@ -29,17 +38,19 @@ export {
ERC20AssetData, ERC20AssetData,
ERC721AssetData, ERC721AssetData,
AssetProxyId, AssetProxyId,
SignerType,
SignatureType, SignatureType,
OrderStateValid, OrderStateValid,
OrderStateInvalid, OrderStateInvalid,
ExchangeContractErrs, ExchangeContractErrs,
EIP712Parameter,
EIP712TypedData,
EIP712Types,
EIP712Object,
EIP712ObjectValue,
ZeroExTransaction,
} from '@0xproject/types'; } from '@0xproject/types';
export { export {
OrderError, OrderError,
EIP712Parameter,
EIP712Schema,
EIP712Types,
TradeSide, TradeSide,
TransferType, TransferType,
FindFeeOrdersThatCoverFeesForTargetOrdersOpts, FindFeeOrdersThatCoverFeesForTargetOrdersOpts,

View File

@ -1,4 +1,4 @@
import { Order, SignedOrder, SignerType } from '@0xproject/types'; import { Order, SignedOrder } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils'; import { BigNumber } from '@0xproject/utils';
import { Provider } from 'ethereum-types'; import { Provider } from 'ethereum-types';
import * as _ from 'lodash'; import * as _ from 'lodash';
@ -71,12 +71,7 @@ export const orderFactory = {
createOrderOpts, createOrderOpts,
); );
const orderHash = orderHashUtils.getOrderHashHex(order); const orderHash = orderHashUtils.getOrderHashHex(order);
const signature = await signatureUtils.ecSignOrderHashAsync( const signature = await signatureUtils.ecSignHashAsync(provider, orderHash, makerAddress);
provider,
orderHash,
makerAddress,
SignerType.Default,
);
const signedOrder: SignedOrder = _.assign(order, { signature }); const signedOrder: SignedOrder = _.assign(order, { signature });
return signedOrder; return signedOrder;
}, },

View File

@ -1,31 +1,13 @@
import { schemas, SchemaValidator } from '@0xproject/json-schemas'; import { schemas, SchemaValidator } from '@0xproject/json-schemas';
import { Order, SignedOrder } from '@0xproject/types'; import { Order, SignedOrder } from '@0xproject/types';
import { signTypedDataUtils } from '@0xproject/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { assert } from './assert'; 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'; const INVALID_TAKER_FORMAT = 'instance.takerAddress is not of a type(s) string';
const EIP712_ORDER_SCHEMA: EIP712Schema = {
name: 'Order',
parameters: [
{ name: 'makerAddress', type: EIP712Types.Address },
{ name: 'takerAddress', type: EIP712Types.Address },
{ name: 'feeRecipientAddress', type: EIP712Types.Address },
{ name: 'senderAddress', type: EIP712Types.Address },
{ name: 'makerAssetAmount', type: EIP712Types.Uint256 },
{ name: 'takerAssetAmount', type: EIP712Types.Uint256 },
{ name: 'makerFee', type: EIP712Types.Uint256 },
{ name: 'takerFee', type: EIP712Types.Uint256 },
{ name: 'expirationTimeSeconds', type: EIP712Types.Uint256 },
{ name: 'salt', type: EIP712Types.Uint256 },
{ name: 'makerAssetData', type: EIP712Types.Bytes },
{ name: 'takerAssetData', type: EIP712Types.Bytes },
],
};
export const orderHashUtils = { export const orderHashUtils = {
/** /**
* Checks if the supplied hex encoded order hash is valid. * Checks if the supplied hex encoded order hash is valid.
@ -45,7 +27,7 @@ export const orderHashUtils = {
/** /**
* Computes the orderHash for a supplied order. * Computes the orderHash for a supplied order.
* @param order An object that conforms to the Order or SignedOrder interface definitions. * @param order An object that conforms to the Order or SignedOrder interface definitions.
* @return The resulting orderHash from hashing the supplied order. * @return Hex encoded string orderHash from hashing the supplied order.
*/ */
getOrderHashHex(order: SignedOrder | Order): string { getOrderHashHex(order: SignedOrder | Order): string {
try { try {
@ -64,16 +46,13 @@ export const orderHashUtils = {
return orderHashHex; return orderHashHex;
}, },
/** /**
* Computes the orderHash for a supplied order and returns it as a Buffer * Computes the orderHash for a supplied order
* @param order An object that conforms to the Order or SignedOrder interface definitions. * @param order An object that conforms to the Order or SignedOrder interface definitions.
* @return The resulting orderHash from hashing the supplied order as a Buffer * @return A Buffer containing the resulting orderHash from hashing the supplied order
*/ */
getOrderHashBuffer(order: SignedOrder | Order): Buffer { getOrderHashBuffer(order: SignedOrder | Order): Buffer {
const orderParamsHashBuff = eip712Utils.structHash(EIP712_ORDER_SCHEMA, order); const typedData = eip712Utils.createOrderTypedData(order);
const orderHashBuff = eip712Utils.createEIP712Message(orderParamsHashBuff, order.exchangeAddress); const orderHashBuff = signTypedDataUtils.generateTypedDataHash(typedData);
return orderHashBuff; return orderHashBuff;
}, },
_getOrderSchemaBuffer(): Buffer {
return eip712Utils.compileSchema(EIP712_ORDER_SCHEMA);
},
}; };

View File

@ -1,5 +1,5 @@
import { schemas } from '@0xproject/json-schemas'; import { schemas } from '@0xproject/json-schemas';
import { ECSignature, SignatureType, SignerType, ValidatorSignature } from '@0xproject/types'; import { ECSignature, Order, SignatureType, SignedOrder, ValidatorSignature } from '@0xproject/types';
import { Web3Wrapper } from '@0xproject/web3-wrapper'; import { Web3Wrapper } from '@0xproject/web3-wrapper';
import { Provider } from 'ethereum-types'; import { Provider } from 'ethereum-types';
import * as ethUtil from 'ethereumjs-util'; import * as ethUtil from 'ethereumjs-util';
@ -7,9 +7,11 @@ import * as _ from 'lodash';
import { artifacts } from './artifacts'; import { artifacts } from './artifacts';
import { assert } from './assert'; import { assert } from './assert';
import { eip712Utils } from './eip712_utils';
import { ExchangeContract } from './generated_contract_wrappers/exchange'; import { ExchangeContract } from './generated_contract_wrappers/exchange';
import { IValidatorContract } from './generated_contract_wrappers/i_validator'; import { IValidatorContract } from './generated_contract_wrappers/i_validator';
import { IWalletContract } from './generated_contract_wrappers/i_wallet'; import { IWalletContract } from './generated_contract_wrappers/i_wallet';
import { orderHashUtils } from './order_hash';
import { OrderError } from './types'; import { OrderError } from './types';
import { utils } from './utils'; import { utils } from './utils';
@ -49,7 +51,7 @@ export const signatureUtils = {
case SignatureType.EthSign: { case SignatureType.EthSign: {
const ecSignature = signatureUtils.parseECSignature(signature); const ecSignature = signatureUtils.parseECSignature(signature);
const prefixedMessageHex = signatureUtils.addSignedMessagePrefix(data, SignerType.Default); const prefixedMessageHex = signatureUtils.addSignedMessagePrefix(data);
return signatureUtils.isValidECSignature(prefixedMessageHex, ecSignature, signerAddress); return signatureUtils.isValidECSignature(prefixedMessageHex, ecSignature, signerAddress);
} }
@ -192,36 +194,90 @@ export const signatureUtils = {
} }
}, },
/** /**
* Signs an orderHash and returns it's elliptic curve signature and signature type. * Signs an order and returns a SignedOrder. First `eth_signTypedData` is requested
* This method currently supports TestRPC, Geth and Parity above and below V1.6.6 * then a fallback to `eth_sign` if not available on the supplied provider.
* @param orderHash Hex encoded orderHash to sign. * @param order The Order to sign.
* @param signerAddress The hex encoded Ethereum address you wish to sign it with. This address * @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. * must be available via the supplied Provider.
* @param signerType Different signers add/require different prefixes to be prepended to the message being signed. * @return A SignedOrder containing the order and Elliptic curve signature with Signature Type.
* 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( async ecSignOrderAsync(provider: Provider, order: Order, signerAddress: string): Promise<SignedOrder> {
provider: Provider, assert.doesConformToSchema('order', order, schemas.orderSchema, [schemas.hexSchema]);
orderHash: string, try {
signerAddress: string, const signedOrder = await signatureUtils.ecSignTypedDataOrderAsync(provider, order, signerAddress);
signerType: SignerType, return signedOrder;
): Promise<string> { } catch (err) {
// HACK: We are unable to handle specific errors thrown since provider is not an object
// under our control. It could be Metamask Web3, Ethers, or any general RPC provider.
// We check for a user denying the signature request in a way that supports Metamask and
// Coinbase Wallet. Unfortunately for signers with a different error message,
// they will receive two signature requests.
if (err.message.includes('User denied message signature')) {
throw err;
}
const orderHash = orderHashUtils.getOrderHashHex(order);
const signatureHex = await signatureUtils.ecSignHashAsync(provider, orderHash, signerAddress);
const signedOrder = {
...order,
signature: signatureHex,
};
return signedOrder;
}
},
/**
* Signs an order using `eth_signTypedData` and returns a SignedOrder.
* @param order The Order to sign.
* @param signerAddress The hex encoded Ethereum address you wish to sign it with. This address
* must be available via the supplied Provider.
* @return A SignedOrder containing the order and Elliptic curve signature with Signature Type.
*/
async ecSignTypedDataOrderAsync(provider: Provider, order: Order, signerAddress: string): Promise<SignedOrder> {
assert.isWeb3Provider('provider', provider); assert.isWeb3Provider('provider', provider);
assert.isHexString('orderHash', orderHash); assert.isETHAddressHex('signerAddress', signerAddress);
assert.doesConformToSchema('order', order, schemas.orderSchema, [schemas.hexSchema]);
const web3Wrapper = new Web3Wrapper(provider);
await assert.isSenderAddressAsync('signerAddress', signerAddress, web3Wrapper);
const normalizedSignerAddress = signerAddress.toLowerCase();
const typedData = eip712Utils.createOrderTypedData(order);
try {
const signature = await web3Wrapper.signTypedDataAsync(normalizedSignerAddress, typedData);
const ecSignatureRSV = parseSignatureHexAsRSV(signature);
const signatureBuffer = Buffer.concat([
ethUtil.toBuffer(ecSignatureRSV.v),
ethUtil.toBuffer(ecSignatureRSV.r),
ethUtil.toBuffer(ecSignatureRSV.s),
ethUtil.toBuffer(SignatureType.EIP712),
]);
const signatureHex = `0x${signatureBuffer.toString('hex')}`;
return {
...order,
signature: signatureHex,
};
} catch (err) {
// Detect if Metamask to transition users to the MetamaskSubprovider
if ((provider as any).isMetaMask) {
throw new Error(OrderError.InvalidMetamaskSigner);
} else {
throw err;
}
}
},
/**
* Signs a hash using `eth_sign` and returns its elliptic curve signature and signature type.
* @param msgHash Hex encoded message to sign.
* @param signerAddress The hex encoded Ethereum address you wish to sign it with. This address
* must be available via the supplied Provider.
* @return A hex encoded string containing the Elliptic curve signature generated by signing the msgHash and the Signature Type.
*/
async ecSignHashAsync(provider: Provider, msgHash: string, signerAddress: string): Promise<string> {
assert.isWeb3Provider('provider', provider);
assert.isHexString('msgHash', msgHash);
assert.isETHAddressHex('signerAddress', signerAddress); assert.isETHAddressHex('signerAddress', signerAddress);
const web3Wrapper = new Web3Wrapper(provider); const web3Wrapper = new Web3Wrapper(provider);
await assert.isSenderAddressAsync('signerAddress', signerAddress, web3Wrapper); await assert.isSenderAddressAsync('signerAddress', signerAddress, web3Wrapper);
const normalizedSignerAddress = signerAddress.toLowerCase(); const normalizedSignerAddress = signerAddress.toLowerCase();
const signature = await web3Wrapper.signMessageAsync(normalizedSignerAddress, msgHash);
let msgHashHex = orderHash; const prefixedMsgHashHex = signatureUtils.addSignedMessagePrefix(msgHash);
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);
// HACK: There is no consensus on whether the signatureHex string should be formatted as // 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) // v + r + s OR r + s + v, and different clients (even different versions of the same client)
@ -238,10 +294,7 @@ export const signatureUtils = {
normalizedSignerAddress, normalizedSignerAddress,
); );
if (isValidRSVSignature) { if (isValidRSVSignature) {
const convertedSignatureHex = signatureUtils.convertECSignatureToSignatureHex( const convertedSignatureHex = signatureUtils.convertECSignatureToSignatureHex(ecSignatureRSV);
ecSignatureRSV,
signerType,
);
return convertedSignatureHex; return convertedSignatureHex;
} }
} }
@ -253,41 +306,30 @@ export const signatureUtils = {
normalizedSignerAddress, normalizedSignerAddress,
); );
if (isValidVRSSignature) { if (isValidVRSSignature) {
const convertedSignatureHex = signatureUtils.convertECSignatureToSignatureHex( const convertedSignatureHex = signatureUtils.convertECSignatureToSignatureHex(ecSignatureVRS);
ecSignatureVRS,
signerType,
);
return convertedSignatureHex; return convertedSignatureHex;
} }
} }
// Detect if Metamask to transition users to the MetamaskSubprovider
throw new Error(OrderError.InvalidSignature); if ((provider as any).isMetaMask) {
throw new Error(OrderError.InvalidMetamaskSigner);
} else {
throw new Error(OrderError.InvalidSignature);
}
}, },
/** /**
* Combines ECSignature with V,R,S and the relevant signature type for use in 0x protocol * Combines ECSignature with V,R,S and the EthSign signature type for use in 0x protocol
* @param ecSignature The ECSignature of the signed data * @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 * @return Hex encoded string of signature (v,r,s) with Signature Type
*/ */
convertECSignatureToSignatureHex(ecSignature: ECSignature, signerType: SignerType): string { convertECSignatureToSignatureHex(ecSignature: ECSignature): string {
const signatureBuffer = Buffer.concat([ const signatureBuffer = Buffer.concat([
ethUtil.toBuffer(ecSignature.v), ethUtil.toBuffer(ecSignature.v),
ethUtil.toBuffer(ecSignature.r), ethUtil.toBuffer(ecSignature.r),
ethUtil.toBuffer(ecSignature.s), ethUtil.toBuffer(ecSignature.s),
]); ]);
const signatureHex = `0x${signatureBuffer.toString('hex')}`; const signatureHex = `0x${signatureBuffer.toString('hex')}`;
let signatureType; const signatureWithType = signatureUtils.convertToSignatureWithType(signatureHex, SignatureType.EthSign);
switch (signerType) {
case SignerType.Metamask:
case SignerType.Ledger:
case SignerType.Default: {
signatureType = SignatureType.EthSign;
break;
}
default:
throw new Error(`Unrecognized SignerType: ${signerType}`);
}
const signatureWithType = signatureUtils.convertToSignatureWithType(signatureHex, signatureType);
return signatureWithType; return signatureWithType;
}, },
/** /**
@ -304,28 +346,17 @@ export const signatureUtils = {
/** /**
* Adds the relevant prefix to the message being signed. * Adds the relevant prefix to the message being signed.
* @param message Message to sign * @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 * @return Prefixed message
*/ */
addSignedMessagePrefix(message: string, signerType: SignerType = SignerType.Default): string { addSignedMessagePrefix(message: string): string {
assert.isString('message', message); assert.isString('message', message);
assert.doesBelongToStringEnum('signerType', signerType, SignerType); const msgBuff = ethUtil.toBuffer(message);
switch (signerType) { const prefixedMsgBuff = ethUtil.hashPersonalMessage(msgBuff);
case SignerType.Metamask: const prefixedMsgHex = ethUtil.bufferToHex(prefixedMsgBuff);
case SignerType.Ledger: return prefixedMsgHex;
case SignerType.Default: {
const msgBuff = ethUtil.toBuffer(message);
const prefixedMsgBuff = ethUtil.hashPersonalMessage(msgBuff);
const prefixedMsgHex = ethUtil.bufferToHex(prefixedMsgBuff);
return prefixedMsgHex;
}
default:
throw new Error(`Unrecognized SignerType: ${signerType}`);
}
}, },
/** /**
* Parse a 0x protocol hex-encoded signature string into it's ECSignature components * Parse a 0x protocol hex-encoded signature string into its ECSignature components
* @param signature A hex encoded ecSignature 0x Protocol signature * @param signature A hex encoded ecSignature 0x Protocol signature
* @return An ECSignature object with r,s,v parameters * @return An ECSignature object with r,s,v parameters
*/ */

View File

@ -2,6 +2,7 @@ import { BigNumber } from '@0xproject/utils';
export enum OrderError { export enum OrderError {
InvalidSignature = 'INVALID_SIGNATURE', InvalidSignature = 'INVALID_SIGNATURE',
InvalidMetamaskSigner = "MetaMask provider must be wrapped in a MetamaskSubprovider (from the '@0xproject/subproviders' package) in order to work with this method.",
} }
export enum TradeSide { export enum TradeSide {
@ -14,24 +15,6 @@ export enum TransferType {
Fee = 'fee', Fee = 'fee',
} }
export interface EIP712Parameter {
name: string;
type: EIP712Types;
}
export interface EIP712Schema {
name: string;
parameters: EIP712Parameter[];
}
export enum EIP712Types {
Address = 'address',
Bytes = 'bytes',
Bytes32 = 'bytes32',
String = 'string',
Uint256 = 'uint256',
}
export interface CreateOrderOpts { export interface CreateOrderOpts {
takerAddress?: string; takerAddress?: string;
senderAddress?: string; senderAddress?: string;

View File

@ -0,0 +1,44 @@
import { BigNumber } from '@0xproject/utils';
import * as chai from 'chai';
import 'mocha';
import { constants } from '../src/constants';
import { eip712Utils } from '../src/eip712_utils';
import { chaiSetup } from './utils/chai_setup';
chaiSetup.configure();
const expect = chai.expect;
describe('EIP712 Utils', () => {
describe('createTypedData', () => {
it('adds in the EIP712DomainSeparator', () => {
const primaryType = 'Test';
const typedData = eip712Utils.createTypedData(
primaryType,
{ Test: [{ name: 'testValue', type: 'uint256' }] },
{ testValue: '1' },
constants.NULL_ADDRESS,
);
expect(typedData.domain).to.not.be.undefined();
expect(typedData.types.EIP712Domain).to.not.be.undefined();
const domainObject = typedData.domain;
expect(domainObject.name).to.eq(constants.EIP712_DOMAIN_NAME);
expect(typedData.primaryType).to.eq(primaryType);
});
});
describe('createTypedData', () => {
it('adds in the EIP712DomainSeparator', () => {
const typedData = eip712Utils.createZeroExTransactionTypedData(
{
salt: new BigNumber('0'),
data: constants.NULL_BYTES,
signerAddress: constants.NULL_ADDRESS,
},
constants.NULL_ADDRESS,
);
expect(typedData.primaryType).to.eq(constants.EIP712_ZEROEX_TRANSACTION_SCHEMA.name);
expect(typedData.types.EIP712Domain).to.not.be.undefined();
});
});
});

View File

@ -35,6 +35,20 @@ describe('Order hashing', () => {
const orderHash = orderHashUtils.getOrderHashHex(order); const orderHash = orderHashUtils.getOrderHashHex(order);
expect(orderHash).to.be.equal(expectedOrderHash); expect(orderHash).to.be.equal(expectedOrderHash);
}); });
it('calculates the order hash if amounts are strings', async () => {
// It's common for developers using javascript to provide the amounts
// as strings. Since we eventually toString() the BigNumber
// before encoding we should result in the same orderHash in this scenario
// tslint:disable-next-line:no-unnecessary-type-assertion
const orderHash = orderHashUtils.getOrderHashHex({
...order,
makerAssetAmount: '0',
takerAssetAmount: '0',
makerFee: '0',
takerFee: '0',
} as any);
expect(orderHash).to.be.equal(expectedOrderHash);
});
it('throws a readable error message if taker format is invalid', async () => { it('throws a readable error message if taker format is invalid', async () => {
const orderWithInvalidtakerFormat = { const orderWithInvalidtakerFormat = {
...order, ...order,

View File

@ -1,12 +1,13 @@
import { SignerType } from '@0xproject/types'; import { Order, SignatureType } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils'; import { BigNumber } from '@0xproject/utils';
import * as chai from 'chai'; import * as chai from 'chai';
import { JSONRPCErrorCallback, JSONRPCRequestPayload } from 'ethereum-types'; import { JSONRPCErrorCallback, JSONRPCRequestPayload } from 'ethereum-types';
import * as ethUtil from 'ethereumjs-util';
import * as _ from 'lodash'; import * as _ from 'lodash';
import 'mocha'; import 'mocha';
import * as Sinon from 'sinon';
import { generatePseudoRandomSalt } from '../src'; import { generatePseudoRandomSalt, orderHashUtils } from '../src';
import { constants } from '../src/constants';
import { signatureUtils } from '../src/signature_utils'; import { signatureUtils } from '../src/signature_utils';
import { chaiSetup } from './utils/chai_setup'; import { chaiSetup } from './utils/chai_setup';
@ -16,6 +17,28 @@ chaiSetup.configure();
const expect = chai.expect; const expect = chai.expect;
describe('Signature utils', () => { describe('Signature utils', () => {
let makerAddress: string;
const fakeExchangeContractAddress = '0x1dc4c1cefef38a777b15aa20260a54e584b16c48';
let order: Order;
before(async () => {
const availableAddreses = await web3Wrapper.getAvailableAddressesAsync();
makerAddress = availableAddreses[0];
order = {
makerAddress,
takerAddress: constants.NULL_ADDRESS,
senderAddress: constants.NULL_ADDRESS,
feeRecipientAddress: constants.NULL_ADDRESS,
makerAssetData: constants.NULL_ADDRESS,
takerAssetData: constants.NULL_ADDRESS,
exchangeAddress: fakeExchangeContractAddress,
salt: new BigNumber(0),
makerFee: new BigNumber(0),
takerFee: new BigNumber(0),
makerAssetAmount: new BigNumber(0),
takerAssetAmount: new BigNumber(0),
expirationTimeSeconds: new BigNumber(0),
};
});
describe('#isValidSignatureAsync', () => { describe('#isValidSignatureAsync', () => {
let dataHex = '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0'; let dataHex = '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0';
const ethSignSignature = const ethSignSignature =
@ -115,28 +138,64 @@ describe('Signature utils', () => {
expect(salt.lessThan(twoPow256)).to.be.true(); expect(salt.lessThan(twoPow256)).to.be.true();
}); });
}); });
describe('#ecSignOrderHashAsync', () => { describe('#ecSignOrderAsync', () => {
let stubs: Sinon.SinonStub[] = []; it('should default to eth_sign if eth_signTypedData is unavailable', async () => {
let makerAddress: string; const expectedSignature =
'0x1c3582f06356a1314dbf1c0e534c4d8e92e59b056ee607a7ff5a825f5f2cc5e6151c5cc7fdd420f5608e4d5bef108e42ad90c7a4b408caef32e24374cf387b0d7603';
const fakeProvider = {
async sendAsync(payload: JSONRPCRequestPayload, callback: JSONRPCErrorCallback): Promise<void> {
if (payload.method === 'eth_signTypedData') {
callback(new Error('Internal RPC Error'));
} else if (payload.method === 'eth_sign') {
const [address, message] = payload.params;
const signature = await web3Wrapper.signMessageAsync(address, message);
callback(null, {
id: 42,
jsonrpc: '2.0',
result: signature,
});
} else {
callback(null, { id: 42, jsonrpc: '2.0', result: [makerAddress] });
}
},
};
const signedOrder = await signatureUtils.ecSignOrderAsync(fakeProvider, order, makerAddress);
expect(signedOrder.signature).to.equal(expectedSignature);
});
it('should throw if the user denies the signing request', async () => {
const fakeProvider = {
async sendAsync(payload: JSONRPCRequestPayload, callback: JSONRPCErrorCallback): Promise<void> {
if (payload.method === 'eth_signTypedData') {
callback(new Error('User denied message signature'));
} else if (payload.method === 'eth_sign') {
const [address, message] = payload.params;
const signature = await web3Wrapper.signMessageAsync(address, message);
callback(null, {
id: 42,
jsonrpc: '2.0',
result: signature,
});
} else {
callback(null, { id: 42, jsonrpc: '2.0', result: [makerAddress] });
}
},
};
expect(signatureUtils.ecSignOrderAsync(fakeProvider, order, makerAddress)).to.to.be.rejectedWith(
'User denied message signature',
);
});
});
describe('#ecSignHashAsync', () => {
before(async () => { before(async () => {
const availableAddreses = await web3Wrapper.getAvailableAddressesAsync(); const availableAddreses = await web3Wrapper.getAvailableAddressesAsync();
makerAddress = availableAddreses[0]; makerAddress = availableAddreses[0];
}); });
afterEach(() => { it('should return the correct Signature', async () => {
// clean up any stubs after the test has completed
_.each(stubs, s => s.restore());
stubs = [];
});
it('Should return the correct Signature', async () => {
const orderHash = '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0'; const orderHash = '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0';
const expectedSignature = const expectedSignature =
'0x1b61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc3340349190569279751135161d22529dc25add4f6069af05be04cacbda2ace225403'; '0x1b61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc3340349190569279751135161d22529dc25add4f6069af05be04cacbda2ace225403';
const ecSignature = await signatureUtils.ecSignOrderHashAsync( const ecSignature = await signatureUtils.ecSignHashAsync(provider, orderHash, makerAddress);
provider,
orderHash,
makerAddress,
SignerType.Default,
);
expect(ecSignature).to.equal(expectedSignature); expect(ecSignature).to.equal(expectedSignature);
}); });
it('should return the correct Signature for signatureHex concatenated as R + S + V', async () => { it('should return the correct Signature for signatureHex concatenated as R + S + V', async () => {
@ -162,12 +221,7 @@ describe('Signature utils', () => {
} }
}, },
}; };
const ecSignature = await signatureUtils.ecSignOrderHashAsync( const ecSignature = await signatureUtils.ecSignHashAsync(fakeProvider, orderHash, makerAddress);
fakeProvider,
orderHash,
makerAddress,
SignerType.Default,
);
expect(ecSignature).to.equal(expectedSignature); expect(ecSignature).to.equal(expectedSignature);
}); });
it('should return the correct Signature for signatureHex concatenated as V + R + S', async () => { it('should return the correct Signature for signatureHex concatenated as V + R + S', async () => {
@ -190,56 +244,12 @@ describe('Signature utils', () => {
}, },
}; };
const ecSignature = await signatureUtils.ecSignOrderHashAsync( const ecSignature = await signatureUtils.ecSignHashAsync(fakeProvider, orderHash, makerAddress);
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,
SignerType.Metamask,
);
expect(ecSignature).to.equal(expectedSignature); expect(ecSignature).to.equal(expectedSignature);
}); });
it('should return a valid signature', async () => { it('should return a valid signature', async () => {
const orderHash = '0x34decbedc118904df65f379a175bb39ca18209d6ce41d5ed549d54e6e0a95004'; const orderHash = '0x34decbedc118904df65f379a175bb39ca18209d6ce41d5ed549d54e6e0a95004';
const ecSignature = await signatureUtils.ecSignOrderHashAsync( const ecSignature = await signatureUtils.ecSignHashAsync(provider, orderHash, makerAddress);
provider,
orderHash,
makerAddress,
SignerType.Default,
);
const isValidSignature = await signatureUtils.isValidSignatureAsync( const isValidSignature = await signatureUtils.isValidSignatureAsync(
provider, provider,
@ -250,44 +260,65 @@ describe('Signature utils', () => {
expect(isValidSignature).to.be.true(); expect(isValidSignature).to.be.true();
}); });
}); });
describe('#ecSignTypedDataOrderAsync', () => {
it('should result in the same signature as signing the order hash without an ethereum message prefix', async () => {
// Note: Since order hash is an EIP712 hash the result of a valid EIP712 signature
// of order hash is the same as signing the order without the Ethereum Message prefix.
const orderHashHex = orderHashUtils.getOrderHashHex(order);
const sig = ethUtil.ecsign(
ethUtil.toBuffer(orderHashHex),
Buffer.from('F2F48EE19680706196E2E339E5DA3491186E0C4C5030670656B0E0164837257D', 'hex'),
);
const signatureBuffer = Buffer.concat([
ethUtil.toBuffer(sig.v),
ethUtil.toBuffer(sig.r),
ethUtil.toBuffer(sig.s),
ethUtil.toBuffer(SignatureType.EIP712),
]);
const signatureHex = `0x${signatureBuffer.toString('hex')}`;
const signedOrder = await signatureUtils.ecSignTypedDataOrderAsync(provider, order, makerAddress);
const isValidSignature = await signatureUtils.isValidSignatureAsync(
provider,
orderHashHex,
signedOrder.signature,
makerAddress,
);
expect(signatureHex).to.eq(signedOrder.signature);
expect(isValidSignature).to.eq(true);
});
it('should return the correct Signature for signatureHex concatenated as R + S + V', async () => {
const expectedSignature =
'0x1cd472c439833774b55d248c31b6585f21aea1b9363ebb4ec58549e46b62eb5a6f696f5781f62de008ee7f77650ef940d99c97ec1dee67b3f5cea1bbfdfeb2eba602';
const fakeProvider = {
async sendAsync(payload: JSONRPCRequestPayload, callback: JSONRPCErrorCallback): Promise<void> {
if (payload.method === 'eth_signTypedData') {
const [address, typedData] = payload.params;
const signature = await web3Wrapper.signTypedDataAsync(address, typedData);
callback(null, {
id: 42,
jsonrpc: '2.0',
result: signature,
});
} else {
callback(null, { id: 42, jsonrpc: '2.0', result: [makerAddress] });
}
},
};
const signedOrder = await signatureUtils.ecSignTypedDataOrderAsync(fakeProvider, order, makerAddress);
expect(signedOrder.signature).to.equal(expectedSignature);
});
});
describe('#convertECSignatureToSignatureHex', () => { describe('#convertECSignatureToSignatureHex', () => {
const ecSignature: ECSignature = { const ecSignature: ECSignature = {
v: 27, v: 27,
r: '0xaca7da997ad177f040240cdccf6905b71ab16b74434388c3a72f34fd25d64393', r: '0xaca7da997ad177f040240cdccf6905b71ab16b74434388c3a72f34fd25d64393',
s: '0x46b2bac274ff29b48b3ea6e2d04c1336eaceafda3c53ab483fc3ff12fac3ebf2', s: '0x46b2bac274ff29b48b3ea6e2d04c1336eaceafda3c53ab483fc3ff12fac3ebf2',
}; };
it('should concatenate v,r,s and append the EthSign signature type when SignerType is Default', async () => { it('should concatenate v,r,s and append the EthSign signature type', async () => {
const expectedSignatureWithSignatureType = const expectedSignatureWithSignatureType =
'0x1baca7da997ad177f040240cdccf6905b71ab16b74434388c3a72f34fd25d6439346b2bac274ff29b48b3ea6e2d04c1336eaceafda3c53ab483fc3ff12fac3ebf203'; '0x1baca7da997ad177f040240cdccf6905b71ab16b74434388c3a72f34fd25d6439346b2bac274ff29b48b3ea6e2d04c1336eaceafda3c53ab483fc3ff12fac3ebf203';
const signatureWithSignatureType = signatureUtils.convertECSignatureToSignatureHex( const signatureWithSignatureType = signatureUtils.convertECSignatureToSignatureHex(ecSignature);
ecSignature,
SignerType.Default,
);
expect(signatureWithSignatureType).to.equal(expectedSignatureWithSignatureType); 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 = signatureUtils.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 = signatureUtils.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(() =>
signatureUtils.convertECSignatureToSignatureHex(ecSignature, 'INVALID_SIGNER' as SignerType),
).to.throw(expectedMessage);
});
}); });
}); });

View File

@ -82,7 +82,7 @@
"bintrees": "^1.0.2", "bintrees": "^1.0.2",
"ethereum-types": "^1.0.11", "ethereum-types": "^1.0.11",
"ethereumjs-blockstream": "6.0.0", "ethereumjs-blockstream": "6.0.0",
"ethers": "4.0.0-beta.14", "ethers": "~4.0.4",
"lodash": "^4.17.5" "lodash": "^4.17.5"
}, },
"publishConfig": { "publishConfig": {

View File

@ -13,4 +13,10 @@ export {
export { OnOrderStateChangeCallback, OrderWatcherConfig } from './types'; export { OnOrderStateChangeCallback, OrderWatcherConfig } from './types';
export { SignedOrder } from '@0xproject/types'; export { SignedOrder } from '@0xproject/types';
export { JSONRPCRequestPayload, JSONRPCErrorCallback, Provider, JSONRPCResponsePayload } from 'ethereum-types'; export {
JSONRPCRequestPayload,
JSONRPCErrorCallback,
Provider,
JSONRPCResponsePayload,
JSONRPCResponseError,
} from 'ethereum-types';

View File

@ -8,7 +8,13 @@ export { ProfilerSubprovider } from './profiler_subprovider';
export { RevertTraceSubprovider } from './revert_trace_subprovider'; export { RevertTraceSubprovider } from './revert_trace_subprovider';
export { ContractData } from './types'; export { ContractData } from './types';
export { JSONRPCRequestPayload, Provider, JSONRPCErrorCallback, JSONRPCResponsePayload } from 'ethereum-types'; export {
JSONRPCRequestPayload,
Provider,
JSONRPCErrorCallback,
JSONRPCResponsePayload,
JSONRPCResponseError,
} from 'ethereum-types';
export { export {
JSONRPCRequestPayloadWithMethod, JSONRPCRequestPayloadWithMethod,

View File

@ -1,4 +1,17 @@
[ [
{
"version": "2.1.0",
"changes": [
{
"note": "Add `MetamaskSubprovider` to handle inconsistent JSON RPC behaviour",
"pr": 1102
},
{
"note": "Add support for `eth_signTypedData` in wallets Mnemonic, Private and EthLightWallet",
"pr": 1102
}
]
},
{ {
"version": "2.0.7", "version": "2.0.7",
"changes": [ "changes": [

View File

@ -45,7 +45,7 @@
"ethereum-types": "^1.0.11", "ethereum-types": "^1.0.11",
"ethereumjs-tx": "^1.3.5", "ethereumjs-tx": "^1.3.5",
"ethereumjs-util": "^5.1.1", "ethereumjs-util": "^5.1.1",
"ganache-core": "0xProject/ganache-core#monorepo-dep", "ganache-core": "^2.2.1",
"hdkey": "^0.7.1", "hdkey": "^0.7.1",
"json-rpc-error": "2.0.0", "json-rpc-error": "2.0.0",
"lodash": "^4.17.5", "lodash": "^4.17.5",

View File

@ -27,6 +27,7 @@ export { Subprovider } from './subproviders/subprovider';
export { NonceTrackerSubprovider } from './subproviders/nonce_tracker'; export { NonceTrackerSubprovider } from './subproviders/nonce_tracker';
export { PrivateKeyWalletSubprovider } from './subproviders/private_key_wallet'; export { PrivateKeyWalletSubprovider } from './subproviders/private_key_wallet';
export { MnemonicWalletSubprovider } from './subproviders/mnemonic_wallet'; export { MnemonicWalletSubprovider } from './subproviders/mnemonic_wallet';
export { MetamaskSubprovider } from './subproviders/metamask_subprovider';
export { EthLightwalletSubprovider } from './subproviders/eth_lightwallet_subprovider'; export { EthLightwalletSubprovider } from './subproviders/eth_lightwallet_subprovider';
export { export {
@ -47,6 +48,19 @@ export {
LedgerGetAddressResult, LedgerGetAddressResult,
} from './types'; } from './types';
export { ECSignature } from '@0xproject/types'; export {
ECSignature,
EIP712Object,
EIP712ObjectValue,
EIP712TypedData,
EIP712Types,
EIP712Parameter,
} from '@0xproject/types';
export { JSONRPCRequestPayload, Provider, JSONRPCResponsePayload, JSONRPCErrorCallback } from 'ethereum-types'; export {
JSONRPCRequestPayload,
Provider,
JSONRPCResponsePayload,
JSONRPCErrorCallback,
JSONRPCResponseError,
} from 'ethereum-types';

View File

@ -23,6 +23,7 @@ export abstract class BaseWalletSubprovider extends Subprovider {
public abstract async getAccountsAsync(): Promise<string[]>; public abstract async getAccountsAsync(): Promise<string[]>;
public abstract async signTransactionAsync(txParams: PartialTxParams): Promise<string>; public abstract async signTransactionAsync(txParams: PartialTxParams): Promise<string>;
public abstract async signPersonalMessageAsync(data: string, address: string): Promise<string>; public abstract async signPersonalMessageAsync(data: string, address: string): Promise<string>;
public abstract async signTypedDataAsync(address: string, typedData: any): Promise<string>;
/** /**
* This method conforms to the web3-provider-engine interface. * This method conforms to the web3-provider-engine interface.
@ -36,6 +37,8 @@ export abstract class BaseWalletSubprovider extends Subprovider {
public async handleRequest(payload: JSONRPCRequestPayload, next: Callback, end: ErrorCallback): Promise<void> { public async handleRequest(payload: JSONRPCRequestPayload, next: Callback, end: ErrorCallback): Promise<void> {
let accounts; let accounts;
let txParams; let txParams;
let address;
let typedData;
switch (payload.method) { switch (payload.method) {
case 'eth_coinbase': case 'eth_coinbase':
try { try {
@ -86,7 +89,7 @@ export abstract class BaseWalletSubprovider extends Subprovider {
case 'eth_sign': case 'eth_sign':
case 'personal_sign': case 'personal_sign':
const data = payload.method === 'eth_sign' ? payload.params[1] : payload.params[0]; const data = payload.method === 'eth_sign' ? payload.params[1] : payload.params[0];
const address = payload.method === 'eth_sign' ? payload.params[0] : payload.params[1]; address = payload.method === 'eth_sign' ? payload.params[0] : payload.params[1];
try { try {
const ecSignatureHex = await this.signPersonalMessageAsync(data, address); const ecSignatureHex = await this.signPersonalMessageAsync(data, address);
end(null, ecSignatureHex); end(null, ecSignatureHex);
@ -94,6 +97,15 @@ export abstract class BaseWalletSubprovider extends Subprovider {
end(err); end(err);
} }
return; return;
case 'eth_signTypedData':
[address, typedData] = payload.params;
try {
const signature = await this.signTypedDataAsync(address, typedData);
end(null, signature);
} catch (err) {
end(err);
}
return;
default: default:
next(); next();

View File

@ -1,3 +1,4 @@
import { EIP712TypedData } from '@0xproject/types';
import * as lightwallet from 'eth-lightwallet'; import * as lightwallet from 'eth-lightwallet';
import { PartialTxParams } from '../types'; import { PartialTxParams } from '../types';
@ -48,16 +49,16 @@ export class EthLightwalletSubprovider extends BaseWalletSubprovider {
// Lightwallet loses the chain id information when hex encoding the transaction // Lightwallet loses the chain id information when hex encoding the transaction
// this results in a different signature on certain networks. PrivateKeyWallet // this results in a different signature on certain networks. PrivateKeyWallet
// respects this as it uses the parameters passed in // respects this as it uses the parameters passed in
let privKey = this._keystore.exportPrivateKey(txParams.from, this._pwDerivedKey); let privateKey = this._keystore.exportPrivateKey(txParams.from, this._pwDerivedKey);
const privKeyWallet = new PrivateKeyWalletSubprovider(privKey); const privateKeyWallet = new PrivateKeyWalletSubprovider(privateKey);
privKey = ''; privateKey = '';
const privKeySignature = await privKeyWallet.signTransactionAsync(txParams); const privateKeySignature = await privateKeyWallet.signTransactionAsync(txParams);
return privKeySignature; return privateKeySignature;
} }
/** /**
* Sign a personal Ethereum signed message. The signing account will be the account * Sign a personal Ethereum signed message. The signing account will be the account
* associated with the provided address. * associated with the provided address.
* If you've added the MnemonicWalletSubprovider to your app's provider, you can simply send an `eth_sign` * If you've added this Subprovider to your app's provider, you can simply send an `eth_sign`
* or `personal_sign` JSON RPC request, and this method will be called auto-magically. * or `personal_sign` JSON RPC request, and this method will be called auto-magically.
* If you are not using this via a ProviderEngine instance, you can call it directly. * If you are not using this via a ProviderEngine instance, you can call it directly.
* @param data Hex string message to sign * @param data Hex string message to sign
@ -65,10 +66,26 @@ export class EthLightwalletSubprovider extends BaseWalletSubprovider {
* @return Signature hex string (order: rsv) * @return Signature hex string (order: rsv)
*/ */
public async signPersonalMessageAsync(data: string, address: string): Promise<string> { public async signPersonalMessageAsync(data: string, address: string): Promise<string> {
let privKey = this._keystore.exportPrivateKey(address, this._pwDerivedKey); let privateKey = this._keystore.exportPrivateKey(address, this._pwDerivedKey);
const privKeyWallet = new PrivateKeyWalletSubprovider(privKey); const privateKeyWallet = new PrivateKeyWalletSubprovider(privateKey);
privKey = ''; privateKey = '';
const result = privKeyWallet.signPersonalMessageAsync(data, address); const result = privateKeyWallet.signPersonalMessageAsync(data, address);
return result;
}
/**
* Sign an EIP712 Typed Data message. The signing address will associated with the provided address.
* If you've added this Subprovider to your app's provider, you can simply send an `eth_signTypedData`
* JSON RPC request, and this method will be called auto-magically.
* If you are not using this via a ProviderEngine instance, you can call it directly.
* @param address Address of the account to sign with
* @param data the typed data object
* @return Signature hex string (order: rsv)
*/
public async signTypedDataAsync(address: string, typedData: EIP712TypedData): Promise<string> {
let privateKey = this._keystore.exportPrivateKey(address, this._pwDerivedKey);
const privateKeyWallet = new PrivateKeyWalletSubprovider(privateKey);
privateKey = '';
const result = privateKeyWallet.signTypedDataAsync(address, typedData);
return result; return result;
} }
} }

View File

@ -187,6 +187,16 @@ export class LedgerSubprovider extends BaseWalletSubprovider {
throw err; throw err;
} }
} }
/**
* eth_signTypedData is currently not supported on Ledger devices.
* @param address Address of the account to sign with
* @param data the typed data object
* @return Signature hex string (order: rsv)
*/
// tslint:disable-next-line:prefer-function-over-method
public async signTypedDataAsync(address: string, typedData: any): Promise<string> {
throw new Error(WalletSubproviderErrors.MethodNotSupported);
}
private async _createLedgerClientAsync(): Promise<LedgerEthereumClient> { private async _createLedgerClientAsync(): Promise<LedgerEthereumClient> {
await this._connectionLock.acquire(); await this._connectionLock.acquire();
if (!_.isUndefined(this._ledgerClientIfExists)) { if (!_.isUndefined(this._ledgerClientIfExists)) {

View File

@ -0,0 +1,126 @@
import { marshaller, Web3Wrapper } from '@0xproject/web3-wrapper';
import { JSONRPCRequestPayload, Provider } from 'ethereum-types';
import * as ethUtil from 'ethereumjs-util';
import { Callback, ErrorCallback } from '../types';
import { Subprovider } from './subprovider';
/**
* This class implements the [web3-provider-engine](https://github.com/MetaMask/provider-engine)
* subprovider interface and the provider sendAsync interface.
* It handles inconsistencies with Metamask implementations of various JSON RPC methods.
* It forwards JSON RPC requests involving the domain of a signer (getAccounts,
* sendTransaction, signMessage etc...) to the provider instance supplied at instantiation. All other requests
* are passed onwards for subsequent subproviders to handle.
*/
export class MetamaskSubprovider extends Subprovider {
private readonly _web3Wrapper: Web3Wrapper;
private readonly _provider: Provider;
/**
* Instantiates a new MetamaskSubprovider
* @param provider Web3 provider that should handle all user account related requests
*/
constructor(provider: Provider) {
super();
this._web3Wrapper = new Web3Wrapper(provider);
this._provider = provider;
}
/**
* This method conforms to the web3-provider-engine interface.
* It is called internally by the ProviderEngine when it is this subproviders
* turn to handle a JSON RPC request.
* @param payload JSON RPC payload
* @param next Callback to call if this subprovider decides not to handle the request
* @param end Callback to call if subprovider handled the request and wants to pass back the request.
*/
// tslint:disable-next-line:prefer-function-over-method async-suffix
public async handleRequest(payload: JSONRPCRequestPayload, next: Callback, end: ErrorCallback): Promise<void> {
let message;
let address;
switch (payload.method) {
case 'web3_clientVersion':
try {
const nodeVersion = await this._web3Wrapper.getNodeVersionAsync();
end(null, nodeVersion);
} catch (err) {
end(err);
}
return;
case 'eth_accounts':
try {
const accounts = await this._web3Wrapper.getAvailableAddressesAsync();
end(null, accounts);
} catch (err) {
end(err);
}
return;
case 'eth_sendTransaction':
const [txParams] = payload.params;
try {
const txData = marshaller.unmarshalTxData(txParams);
const txHash = await this._web3Wrapper.sendTransactionAsync(txData);
end(null, txHash);
} catch (err) {
end(err);
}
return;
case 'eth_sign':
[address, message] = payload.params;
try {
// Metamask incorrectly implements eth_sign and does not prefix the message as per the spec
// Source: https://github.com/MetaMask/metamask-extension/commit/a9d36860bec424dcee8db043d3e7da6a5ff5672e
const msgBuff = ethUtil.toBuffer(message);
const prefixedMsgBuff = ethUtil.hashPersonalMessage(msgBuff);
const prefixedMsgHex = ethUtil.bufferToHex(prefixedMsgBuff);
const signature = await this._web3Wrapper.signMessageAsync(address, prefixedMsgHex);
signature ? end(null, signature) : end(new Error('Error performing eth_sign'), null);
} catch (err) {
end(err);
}
return;
case 'eth_signTypedData':
case 'eth_signTypedData_v3':
[address, message] = payload.params;
try {
// Metamask supports multiple versions and has namespaced signTypedData to v3 for an indeterminate period of time.
// eth_signTypedData is mapped to an older implementation before the spec was finalized.
// Source: https://github.com/MetaMask/metamask-extension/blob/c49d854b55b3efd34c7fd0414b76f7feaa2eec7c/app/scripts/metamask-controller.js#L1262
// and expects message to be serialised as JSON
const messageJSON = JSON.stringify(message);
const signature = await this._web3Wrapper.sendRawPayloadAsync<string>({
method: 'eth_signTypedData_v3',
params: [address, messageJSON],
});
signature ? end(null, signature) : end(new Error('Error performing eth_signTypedData'), null);
} catch (err) {
end(err);
}
return;
default:
next();
return;
}
}
/**
* This method conforms to the provider sendAsync interface.
* Allowing the MetamaskSubprovider to be used as a generic provider (outside of Web3ProviderEngine) with the
* addition of wrapping the inconsistent Metamask behaviour
* @param payload JSON RPC payload
* @return The contents nested under the result key of the response body
*/
public sendAsync(payload: JSONRPCRequestPayload, callback: ErrorCallback): void {
void this.handleRequest(
payload,
// handleRequest has decided to not handle this, so fall through to the provider
() => {
const sendAsync = this._provider.sendAsync.bind(this._provider);
sendAsync(payload, callback);
},
// handleRequest has called end and will handle this
(err, data) => {
err ? callback(err) : callback(null, { ...payload, result: data });
},
);
}
}

View File

@ -1,4 +1,5 @@
import { assert } from '@0xproject/assert'; import { assert } from '@0xproject/assert';
import { EIP712TypedData } from '@0xproject/types';
import { addressUtils } from '@0xproject/utils'; import { addressUtils } from '@0xproject/utils';
import * as bip39 from 'bip39'; import * as bip39 from 'bip39';
import HDNode = require('hdkey'); import HDNode = require('hdkey');
@ -90,10 +91,10 @@ export class MnemonicWalletSubprovider extends BaseWalletSubprovider {
} }
/** /**
* Sign a personal Ethereum signed message. The signing account will be the account * Sign a personal Ethereum signed message. The signing account will be the account
* associated with the provided address. * associated with the provided address. If you've added the MnemonicWalletSubprovider to
* If you've added the MnemonicWalletSubprovider to your app's provider, you can simply send an `eth_sign` * your app's provider, you can simply send an `eth_sign` or `personal_sign` JSON RPC request,
* or `personal_sign` JSON RPC request, and this method will be called auto-magically. * and this method will be called auto-magically. If you are not using this via a ProviderEngine
* If you are not using this via a ProviderEngine instance, you can call it directly. * instance, you can call it directly.
* @param data Hex string message to sign * @param data Hex string message to sign
* @param address Address of the account to sign with * @param address Address of the account to sign with
* @return Signature hex string (order: rsv) * @return Signature hex string (order: rsv)
@ -108,6 +109,25 @@ export class MnemonicWalletSubprovider extends BaseWalletSubprovider {
const sig = await privateKeyWallet.signPersonalMessageAsync(data, address); const sig = await privateKeyWallet.signPersonalMessageAsync(data, address);
return sig; return sig;
} }
/**
* Sign an EIP712 Typed Data message. The signing account will be the account
* associated with the provided address. If you've added this MnemonicWalletSubprovider to
* your app's provider, you can simply send an `eth_signTypedData` JSON RPC request, and
* this method will be called auto-magically. If you are not using this via a ProviderEngine
* instance, you can call it directly.
* @param address Address of the account to sign with
* @param data the typed data object
* @return Signature hex string (order: rsv)
*/
public async signTypedDataAsync(address: string, typedData: EIP712TypedData): Promise<string> {
if (_.isUndefined(typedData)) {
throw new Error(WalletSubproviderErrors.DataMissingForSignPersonalMessage);
}
assert.isETHAddressHex('address', address);
const privateKeyWallet = this._privateKeyWalletForAddress(address);
const sig = await privateKeyWallet.signTypedDataAsync(address, typedData);
return sig;
}
private _privateKeyWalletForAddress(address: string): PrivateKeyWalletSubprovider { private _privateKeyWalletForAddress(address: string): PrivateKeyWalletSubprovider {
const derivedKeyInfo = this._findDerivedKeyInfoForAddress(address); const derivedKeyInfo = this._findDerivedKeyInfoForAddress(address);
const privateKeyHex = derivedKeyInfo.hdKey.privateKey.toString('hex'); const privateKeyHex = derivedKeyInfo.hdKey.privateKey.toString('hex');

View File

@ -1,4 +1,6 @@
import { assert } from '@0xproject/assert'; import { assert } from '@0xproject/assert';
import { EIP712TypedData } from '@0xproject/types';
import { signTypedDataUtils } from '@0xproject/utils';
import EthereumTx = require('ethereumjs-tx'); import EthereumTx = require('ethereumjs-tx');
import * as ethUtil from 'ethereumjs-util'; import * as ethUtil from 'ethereumjs-util';
import * as _ from 'lodash'; import * as _ from 'lodash';
@ -23,7 +25,7 @@ export class PrivateKeyWalletSubprovider extends BaseWalletSubprovider {
constructor(privateKey: string) { constructor(privateKey: string) {
assert.isString('privateKey', privateKey); assert.isString('privateKey', privateKey);
super(); super();
this._privateKeyBuffer = new Buffer(privateKey, 'hex'); this._privateKeyBuffer = Buffer.from(privateKey, 'hex');
this._address = `0x${ethUtil.privateToAddress(this._privateKeyBuffer).toString('hex')}`; this._address = `0x${ethUtil.privateToAddress(this._privateKeyBuffer).toString('hex')}`;
} }
/** /**
@ -84,4 +86,29 @@ export class PrivateKeyWalletSubprovider extends BaseWalletSubprovider {
const rpcSig = ethUtil.toRpcSig(sig.v, sig.r, sig.s); const rpcSig = ethUtil.toRpcSig(sig.v, sig.r, sig.s);
return rpcSig; return rpcSig;
} }
/**
* Sign an EIP712 Typed Data message. The signing address will be calculated from the private key.
* The address must be provided it must match the address calculated from the private key.
* If you've added this Subprovider to your app's provider, you can simply send an `eth_signTypedData`
* JSON RPC request, and this method will be called auto-magically.
* If you are not using this via a ProviderEngine instance, you can call it directly.
* @param address Address of the account to sign with
* @param data the typed data object
* @return Signature hex string (order: rsv)
*/
public async signTypedDataAsync(address: string, typedData: EIP712TypedData): Promise<string> {
if (_.isUndefined(typedData)) {
throw new Error(WalletSubproviderErrors.DataMissingForSignTypedData);
}
assert.isETHAddressHex('address', address);
if (address !== this._address) {
throw new Error(
`Requested to sign message with address: ${address}, instantiated with address: ${this._address}`,
);
}
const dataBuff = signTypedDataUtils.generateTypedDataHash(typedData);
const sig = ethUtil.ecsign(dataBuff, this._privateKeyBuffer);
const rpcSig = ethUtil.toRpcSig(sig.v, sig.r, sig.s);
return rpcSig;
}
} }

Some files were not shown because too many files have changed in this diff Show More