Merge branch 'development' into development
This commit is contained in:
commit
69e9dbd683
@ -23,6 +23,7 @@ jobs:
|
||||
paths:
|
||||
- ~/repo
|
||||
build-website:
|
||||
resource_class: medium+
|
||||
docker:
|
||||
- image: circleci/node:9
|
||||
working_directory: ~/repo
|
||||
@ -161,6 +162,90 @@ jobs:
|
||||
key: coverage-web3-wrapper-{{ .Environment.CIRCLE_SHA1 }}
|
||||
paths:
|
||||
- ~/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:
|
||||
working_directory: ~/repo
|
||||
docker:
|
||||
@ -172,7 +257,7 @@ jobs:
|
||||
- run: yarn lerna run lint
|
||||
- run: yarn prettier:ci
|
||||
- run: cd packages/0x.js && yarn build:umd:prod
|
||||
- run: yarn bundlesize
|
||||
- run: yarn bundlewatch
|
||||
submit-coverage:
|
||||
docker:
|
||||
- image: circleci/node:9
|
||||
@ -232,6 +317,9 @@ jobs:
|
||||
- restore_cache:
|
||||
keys:
|
||||
- coverage-contracts-{{ .Environment.CIRCLE_SHA1 }}
|
||||
- restore_cache:
|
||||
keys:
|
||||
- coverage-python-order-utils-{{ .Environment.CIRCLE_SHA1 }}
|
||||
- run: yarn report_coverage
|
||||
workflows:
|
||||
version: 2
|
||||
@ -262,3 +350,8 @@ workflows:
|
||||
- submit-coverage:
|
||||
requires:
|
||||
- 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
10
.gitignore
vendored
@ -72,6 +72,7 @@ TODO.md
|
||||
.vscode
|
||||
|
||||
packages/website/public/bundle*
|
||||
packages/dev-tools-pages/public/bundle*
|
||||
packages/react-docs/example/public/bundle*
|
||||
|
||||
# server cli
|
||||
@ -108,3 +109,12 @@ packages/sol-compiler/solc_bin/
|
||||
|
||||
# Monorepo scripts
|
||||
packages/*/scripts/
|
||||
|
||||
# python stuff
|
||||
.eggs
|
||||
.mypy_cache
|
||||
.tox
|
||||
python-packages/*/build
|
||||
__pycache__
|
||||
python-packages/*/src/*.egg-info
|
||||
python-packages/*/.coverage
|
||||
|
22
package.json
22
package.json
@ -11,7 +11,7 @@
|
||||
"ganache": "ganache-cli -p 8545 --networkId 50 -m \"${npm_package_config_mnemonic}\"",
|
||||
"prettier": "prettier --write '**/*.{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: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.",
|
||||
@ -36,27 +36,35 @@
|
||||
"test": "wsrun test $PKG --fast-exit --serial --exclude-missing",
|
||||
"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;",
|
||||
"bundlesize": "bundlesize",
|
||||
"bundlewatch": "bundlewatch",
|
||||
"lint": "wsrun lint $PKG --fast-exit --parallel --exclude-missing"
|
||||
},
|
||||
"config": {
|
||||
"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"
|
||||
},
|
||||
"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": {
|
||||
"@0x-lerna-fork/lerna": "3.0.0-beta.25",
|
||||
"async-child-process": "^1.1.1",
|
||||
"bundlesize": "^0.17.0",
|
||||
"bundlewatch": "^0.2.1",
|
||||
"coveralls": "^3.0.0",
|
||||
"ganache-cli": "6.1.3",
|
||||
"ganache-cli": "6.1.8",
|
||||
"lcov-result-merger": "^3.0.0",
|
||||
"npm-cli-login": "^0.0.10",
|
||||
"npm-run-all": "^4.1.2",
|
||||
|
@ -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",
|
||||
"changes": [
|
||||
|
@ -84,7 +84,7 @@
|
||||
"@0xproject/utils": "^2.0.2",
|
||||
"@0xproject/web3-wrapper": "^3.0.3",
|
||||
"ethereum-types": "^1.0.11",
|
||||
"ethers": "4.0.0-beta.14",
|
||||
"ethers": "~4.0.4",
|
||||
"lodash": "^4.17.5",
|
||||
"web3-provider-engine": "14.0.6"
|
||||
},
|
||||
|
@ -53,7 +53,13 @@ export { OrderWatcher, OnOrderStateChangeCallback, OrderWatcherConfig } from '@0
|
||||
|
||||
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';
|
||||
|
||||
@ -68,7 +74,6 @@ export {
|
||||
OrderStateInvalid,
|
||||
OrderState,
|
||||
AssetProxyId,
|
||||
SignerType,
|
||||
ERC20AssetData,
|
||||
ERC721AssetData,
|
||||
SignatureType,
|
||||
@ -85,6 +90,7 @@ export {
|
||||
JSONRPCRequestPayload,
|
||||
JSONRPCResponsePayload,
|
||||
JSONRPCErrorCallback,
|
||||
JSONRPCResponseError,
|
||||
LogEntry,
|
||||
DecodedLogArgs,
|
||||
LogEntryEvent,
|
||||
|
@ -1,4 +1,12 @@
|
||||
[
|
||||
{
|
||||
"version": "2.1.0",
|
||||
"changes": [
|
||||
{
|
||||
"note": "Add `gasLimit` and `gasPrice` as optional properties on `BuyQuoteExecutionOpts`"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"changes": [
|
||||
|
@ -183,7 +183,7 @@ export class AssetBuyer {
|
||||
buyQuote: BuyQuote,
|
||||
options: Partial<BuyQuoteExecutionOpts> = {},
|
||||
): Promise<string> {
|
||||
const { ethAmount, takerAddress, feeRecipient } = {
|
||||
const { ethAmount, takerAddress, feeRecipient, gasLimit, gasPrice } = {
|
||||
...constants.DEFAULT_BUY_QUOTE_EXECUTION_OPTS,
|
||||
...options,
|
||||
};
|
||||
@ -219,6 +219,10 @@ export class AssetBuyer {
|
||||
feeOrders,
|
||||
feePercentage,
|
||||
feeRecipient,
|
||||
{
|
||||
gasLimit,
|
||||
gasPrice,
|
||||
},
|
||||
);
|
||||
return txHash;
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ const MAINNET_NETWORK_ID = 1;
|
||||
const DEFAULT_ASSET_BUYER_OPTS: AssetBuyerOpts = {
|
||||
networkId: MAINNET_NETWORK_ID,
|
||||
orderRefreshIntervalMs: 10000, // 10 seconds
|
||||
expiryBufferSeconds: 15,
|
||||
expiryBufferSeconds: 300, // 5 minutes
|
||||
};
|
||||
|
||||
const DEFAULT_BUY_QUOTE_REQUEST_OPTS: BuyQuoteRequestOpts = {
|
||||
|
@ -77,18 +77,22 @@ export interface BuyQuoteRequestOpts {
|
||||
/**
|
||||
* 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.
|
||||
* 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).
|
||||
*/
|
||||
export interface BuyQuoteExecutionOpts {
|
||||
ethAmount?: BigNumber;
|
||||
takerAddress?: string;
|
||||
gasLimit?: number;
|
||||
gasPrice?: BigNumber;
|
||||
feeRecipient: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
* 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 {
|
||||
networkId: number;
|
||||
|
@ -72,7 +72,6 @@ export const buyQuoteCalculator = {
|
||||
assetBuyAmount,
|
||||
feePercentage,
|
||||
);
|
||||
|
||||
return {
|
||||
assetData,
|
||||
orders: resultOrders,
|
||||
@ -98,13 +97,14 @@ function calculateQuoteInfo(
|
||||
);
|
||||
// find the total eth needed to buy fees
|
||||
const ethAmountToBuyFees = findEthAmountNeededToBuyFees(feeOrdersAndFillableAmounts, zrxAmountToBuyAsset);
|
||||
const ethAmountBeforeAffiliateFee = ethAmountToBuyAsset.plus(ethAmountToBuyFees);
|
||||
const totalEthAmount = ethAmountBeforeAffiliateFee.mul(feePercentage + 1);
|
||||
const affiliateFeeEthAmount = ethAmountToBuyAsset.mul(feePercentage);
|
||||
const totalEthAmountWithoutAffiliateFee = ethAmountToBuyAsset.plus(ethAmountToBuyFees);
|
||||
const totalEthAmount = totalEthAmountWithoutAffiliateFee.plus(affiliateFeeEthAmount);
|
||||
// divide into the assetBuyAmount in order to find rate of makerAsset / WETH
|
||||
const ethPerAssetPrice = ethAmountBeforeAffiliateFee.div(assetBuyAmount);
|
||||
const ethPerAssetPrice = totalEthAmountWithoutAffiliateFee.div(assetBuyAmount);
|
||||
return {
|
||||
totalEthAmount,
|
||||
feeEthAmount: totalEthAmount.minus(ethAmountBeforeAffiliateFee),
|
||||
feeEthAmount: affiliateFeeEthAmount,
|
||||
ethPerAssetPrice,
|
||||
};
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ export const orderUtils = {
|
||||
willOrderExpire(order: SignedOrder, secondsFromNow: number): boolean {
|
||||
const millisecondsInSecond = 1000;
|
||||
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 {
|
||||
if (remainingTakerAssetAmount.eq(0)) {
|
||||
|
@ -103,9 +103,11 @@ describe('buyQuoteCalculator', () => {
|
||||
expect(buyQuote.feeOrders).to.deep.equal([smallFeeOrderAndFillableAmount.orders[0]]);
|
||||
// test if rates are correct
|
||||
// 50 eth to fill the first order + 100 eth for fees
|
||||
const expectedFillEthAmount = new BigNumber(150);
|
||||
const expectedTotalEthAmount = expectedFillEthAmount.mul(feePercentage + 1);
|
||||
const expectedFeeEthAmount = expectedTotalEthAmount.minus(expectedFillEthAmount);
|
||||
const expectedEthAmountForAsset = new BigNumber(50);
|
||||
const expectedEthAmountForZrxFees = new BigNumber(100);
|
||||
const expectedFillEthAmount = expectedEthAmountForAsset.plus(expectedEthAmountForZrxFees);
|
||||
const expectedFeeEthAmount = expectedEthAmountForAsset.mul(feePercentage);
|
||||
const expectedTotalEthAmount = expectedFillEthAmount.plus(expectedFeeEthAmount);
|
||||
const expectedEthPerAssetPrice = expectedFillEthAmount.div(assetBuyAmount);
|
||||
expect(buyQuote.bestCaseQuoteInfo.feeEthAmount).to.bignumber.equal(expectedFeeEthAmount);
|
||||
expect(buyQuote.bestCaseQuoteInfo.totalEthAmount).to.bignumber.equal(expectedTotalEthAmount);
|
||||
@ -138,17 +140,21 @@ describe('buyQuoteCalculator', () => {
|
||||
expect(buyQuote.feeOrders).to.deep.equal(allFeeOrdersAndFillableAmounts.orders);
|
||||
// test if rates are correct
|
||||
// 50 eth to fill the first order + 100 eth for fees
|
||||
const expectedFillEthAmount = new BigNumber(150);
|
||||
const expectedTotalEthAmount = expectedFillEthAmount.mul(feePercentage + 1);
|
||||
const expectedFeeEthAmount = expectedTotalEthAmount.minus(expectedFillEthAmount);
|
||||
const expectedEthAmountForAsset = new BigNumber(50);
|
||||
const expectedEthAmountForZrxFees = new BigNumber(100);
|
||||
const expectedFillEthAmount = expectedEthAmountForAsset.plus(expectedEthAmountForZrxFees);
|
||||
const expectedFeeEthAmount = expectedEthAmountForAsset.mul(feePercentage);
|
||||
const expectedTotalEthAmount = expectedFillEthAmount.plus(expectedFeeEthAmount);
|
||||
const expectedEthPerAssetPrice = expectedFillEthAmount.div(assetBuyAmount);
|
||||
expect(buyQuote.bestCaseQuoteInfo.feeEthAmount).to.bignumber.equal(expectedFeeEthAmount);
|
||||
expect(buyQuote.bestCaseQuoteInfo.totalEthAmount).to.bignumber.equal(expectedTotalEthAmount);
|
||||
expect(buyQuote.bestCaseQuoteInfo.ethPerAssetPrice).to.bignumber.equal(expectedEthPerAssetPrice);
|
||||
// 100 eth to fill the first order + 200 eth for fees
|
||||
const expectedWorstFillEthAmount = new BigNumber(300);
|
||||
const expectedWorstTotalEthAmount = expectedWorstFillEthAmount.mul(feePercentage + 1);
|
||||
const expectedWorstFeeEthAmount = expectedWorstTotalEthAmount.minus(expectedWorstFillEthAmount);
|
||||
const expectedWorstEthAmountForAsset = new BigNumber(100);
|
||||
const expectedWorstEthAmountForZrxFees = new BigNumber(200);
|
||||
const expectedWorstFillEthAmount = expectedWorstEthAmountForAsset.plus(expectedWorstEthAmountForZrxFees);
|
||||
const expectedWorstFeeEthAmount = expectedWorstEthAmountForAsset.mul(feePercentage);
|
||||
const expectedWorstTotalEthAmount = expectedWorstFillEthAmount.plus(expectedWorstFeeEthAmount);
|
||||
const expectedWorstEthPerAssetPrice = expectedWorstFillEthAmount.div(assetBuyAmount);
|
||||
expect(buyQuote.worstCaseQuoteInfo.feeEthAmount).to.bignumber.equal(expectedWorstFeeEthAmount);
|
||||
expect(buyQuote.worstCaseQuoteInfo.totalEthAmount).to.bignumber.equal(expectedWorstTotalEthAmount);
|
||||
|
@ -45,7 +45,7 @@
|
||||
"@0xproject/utils": "^2.0.2",
|
||||
"@0xproject/web3-wrapper": "^3.0.3",
|
||||
"ethereum-types": "^1.0.11",
|
||||
"ethers": "4.0.0-beta.14",
|
||||
"ethers": "~4.0.4",
|
||||
"lodash": "^4.17.5"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
@ -17,7 +17,7 @@ import * as _ from 'lodash';
|
||||
import { formatABIDataItem } from './utils';
|
||||
|
||||
export interface EthersInterfaceByFunctionSignature {
|
||||
[key: string]: ethers.Interface;
|
||||
[key: string]: ethers.utils.Interface;
|
||||
}
|
||||
|
||||
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
|
||||
// encoder, or the encoder performs unsafe type coercion.
|
||||
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 rawEncoded = coder.encode(inputAbi, args);
|
||||
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];
|
||||
if (_.isUndefined(ethersInterface)) {
|
||||
throw new Error(`Failed to lookup method with function signature '${functionSignature}'`);
|
||||
@ -154,7 +154,7 @@ export class BaseContract {
|
||||
this._ethersInterfacesByFunctionSignature = {};
|
||||
_.each(methodAbis, methodAbi => {
|
||||
const functionSignature = abiUtils.getFunctionSignature(methodAbi);
|
||||
this._ethersInterfacesByFunctionSignature[functionSignature] = new ethers.Interface([methodAbi]);
|
||||
this._ethersInterfacesByFunctionSignature[functionSignature] = new ethers.utils.Interface([methodAbi]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -84,7 +84,7 @@
|
||||
"ethereum-types": "^1.0.11",
|
||||
"ethereumjs-blockstream": "6.0.0",
|
||||
"ethereumjs-util": "^5.1.1",
|
||||
"ethers": "4.0.0-beta.14",
|
||||
"ethers": "~4.0.4",
|
||||
"js-sha3": "^0.7.0",
|
||||
"lodash": "^4.17.5",
|
||||
"uuid": "^3.1.0"
|
||||
|
@ -39,6 +39,7 @@ export {
|
||||
JSONRPCRequestPayload,
|
||||
JSONRPCResponsePayload,
|
||||
JSONRPCErrorCallback,
|
||||
JSONRPCResponseError,
|
||||
AbiDefinition,
|
||||
LogWithDecodedArgs,
|
||||
FunctionAbi,
|
||||
|
@ -1,22 +1,13 @@
|
||||
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 { BigNumber } from '@0xproject/utils';
|
||||
import { BigNumber, signTypedDataUtils } from '@0xproject/utils';
|
||||
import _ = require('lodash');
|
||||
|
||||
import { ExchangeContract } from '../contract_wrappers/generated/exchange';
|
||||
|
||||
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
|
||||
* 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,
|
||||
data,
|
||||
};
|
||||
const executeTransactionHashBuff = eip712Utils.structHash(
|
||||
EIP712_ZEROEX_TRANSACTION_SCHEMA,
|
||||
executeTransactionData,
|
||||
);
|
||||
const eip721MessageBuffer = eip712Utils.createEIP712Message(executeTransactionHashBuff, exchangeAddress);
|
||||
const messageHex = `0x${eip721MessageBuffer.toString('hex')}`;
|
||||
const typedData = eip712Utils.createZeroExTransactionTypedData(executeTransactionData, exchangeAddress);
|
||||
const eip712MessageBuffer = signTypedDataUtils.generateTypedDataHash(typedData);
|
||||
const messageHex = `0x${eip712MessageBuffer.toString('hex')}`;
|
||||
return messageHex;
|
||||
}
|
||||
/**
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { BlockchainLifecycle } from '@0xproject/dev-utils';
|
||||
import { FillScenarios } from '@0xproject/fill-scenarios';
|
||||
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 'mocha';
|
||||
|
||||
@ -80,12 +80,7 @@ describe('TransactionEncoder', () => {
|
||||
): Promise<void> => {
|
||||
const salt = generatePseudoRandomSalt();
|
||||
const encodedTransaction = encoder.getTransactionHex(data, salt, signerAddress);
|
||||
const signature = await signatureUtils.ecSignOrderHashAsync(
|
||||
provider,
|
||||
encodedTransaction,
|
||||
signerAddress,
|
||||
SignerType.Default,
|
||||
);
|
||||
const signature = await signatureUtils.ecSignHashAsync(provider, encodedTransaction, signerAddress);
|
||||
txHash = await contractWrappers.exchange.executeTransactionAsync(
|
||||
salt,
|
||||
signerAddress,
|
||||
|
@ -65,7 +65,7 @@ export class {{contractName}}Contract extends BaseContract {
|
||||
[{{> params inputs=ctor.inputs}}],
|
||||
BaseContract._bigNumberToString,
|
||||
);
|
||||
const iface = new ethers.Interface(abi);
|
||||
const iface = new ethers.utils.Interface(abi);
|
||||
const deployInfo = iface.deployFunction;
|
||||
const txData = deployInfo.encode(bytecode, [{{> params inputs=ctor.inputs}}]);
|
||||
const web3Wrapper = new Web3Wrapper(provider);
|
||||
|
@ -84,7 +84,7 @@
|
||||
"ethereum-types": "^1.0.11",
|
||||
"ethereumjs-abi": "0.6.5",
|
||||
"ethereumjs-util": "^5.1.1",
|
||||
"ethers": "4.0.0-beta.14",
|
||||
"ethers": "~4.0.4",
|
||||
"js-combinatorics": "^0.5.3",
|
||||
"lodash": "^4.17.5"
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { BigNumber } from '@0xproject/utils';
|
||||
import * as chai from 'chai';
|
||||
@ -126,22 +126,6 @@ describe('Exchange libs', () => {
|
||||
});
|
||||
|
||||
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', () => {
|
||||
it('should output the correct orderHash', async () => {
|
||||
signedOrder = await orderFactory.newSignedOrderAsync();
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { BlockchainLifecycle } from '@0xproject/dev-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 { LogWithDecodedArgs } from 'ethereum-types';
|
||||
import ethUtil = require('ethereumjs-util');
|
||||
@ -231,10 +231,7 @@ describe('MixinSignatureValidator', () => {
|
||||
it('should return true when SignatureType=EthSign and signature is valid', async () => {
|
||||
// Create EthSign signature
|
||||
const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
|
||||
const orderHashWithEthSignPrefixHex = signatureUtils.addSignedMessagePrefix(
|
||||
orderHashHex,
|
||||
SignerType.Default,
|
||||
);
|
||||
const orderHashWithEthSignPrefixHex = signatureUtils.addSignedMessagePrefix(orderHashHex);
|
||||
const orderHashWithEthSignPrefixBuffer = ethUtil.toBuffer(orderHashWithEthSignPrefixHex);
|
||||
const ecSignature = ethUtil.ecsign(orderHashWithEthSignPrefixBuffer, signerPrivateKey);
|
||||
// Create 0x signature from EthSign signature
|
||||
@ -257,10 +254,7 @@ describe('MixinSignatureValidator', () => {
|
||||
it('should return false when SignatureType=EthSign and signature is invalid', async () => {
|
||||
// Create EthSign signature
|
||||
const orderHashHex = orderHashUtils.getOrderHashHex(signedOrder);
|
||||
const orderHashWithEthSignPrefixHex = signatureUtils.addSignedMessagePrefix(
|
||||
orderHashHex,
|
||||
SignerType.Default,
|
||||
);
|
||||
const orderHashWithEthSignPrefixHex = signatureUtils.addSignedMessagePrefix(orderHashHex);
|
||||
const orderHashWithEthSignPrefixBuffer = ethUtil.toBuffer(orderHashWithEthSignPrefixHex);
|
||||
const ecSignature = ethUtil.ecsign(orderHashWithEthSignPrefixBuffer, signerPrivateKey);
|
||||
// Create 0x signature from EthSign signature
|
||||
|
@ -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 { signTypedDataUtils } from '@0xproject/utils';
|
||||
import * as ethUtil from 'ethereumjs-util';
|
||||
|
||||
import { signingUtils } from './signing_utils';
|
||||
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 {
|
||||
private readonly _signerBuff: Buffer;
|
||||
private readonly _exchangeAddress: string;
|
||||
@ -31,12 +23,10 @@ export class TransactionFactory {
|
||||
signerAddress,
|
||||
data,
|
||||
};
|
||||
const executeTransactionHashBuff = eip712Utils.structHash(
|
||||
EIP712_ZEROEX_TRANSACTION_SCHEMA,
|
||||
executeTransactionData,
|
||||
);
|
||||
const txHash = eip712Utils.createEIP712Message(executeTransactionHashBuff, this._exchangeAddress);
|
||||
const signature = signingUtils.signMessage(txHash, this._privateKey, signatureType);
|
||||
|
||||
const typedData = eip712Utils.createZeroExTransactionTypedData(executeTransactionData, this._exchangeAddress);
|
||||
const eip712MessageBuffer = signTypedDataUtils.generateTypedDataHash(typedData);
|
||||
const signature = signingUtils.signMessage(eip712MessageBuffer, this._privateKey, signatureType);
|
||||
const signedTx = {
|
||||
exchangeAddress: this._exchangeAddress,
|
||||
signature: `0x${signature.toString('hex')}`,
|
||||
|
88
packages/dev-tools-pages/README.md
Normal file
88
packages/dev-tools-pages/README.md
Normal 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.
|
0
packages/dev-tools-pages/less/all.less
Normal file
0
packages/dev-tools-pages/less/all.less
Normal file
58
packages/dev-tools-pages/package.json
Normal file
58
packages/dev-tools-pages/package.json
Normal 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"
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 }
|
||||
|
||||
}
|
@ -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 }
|
||||
}
|
BIN
packages/dev-tools-pages/public/images/favicon/favicon-2-16x16.png
Executable file
BIN
packages/dev-tools-pages/public/images/favicon/favicon-2-16x16.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 684 B |
BIN
packages/dev-tools-pages/public/images/favicon/favicon-2-32x32.png
Executable file
BIN
packages/dev-tools-pages/public/images/favicon/favicon-2-32x32.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
BIN
packages/dev-tools-pages/public/images/favicon/favicon.ico
Executable file
BIN
packages/dev-tools-pages/public/images/favicon/favicon.ico
Executable file
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
26
packages/dev-tools-pages/public/index.html
Normal file
26
packages/dev-tools-pages/public/index.html
Normal 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>
|
25
packages/dev-tools-pages/ts/components/meta_tags.tsx
Normal file
25
packages/dev-tools-pages/ts/components/meta_tags.tsx
Normal 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',
|
||||
};
|
59
packages/dev-tools-pages/ts/components/ui/button.tsx
Normal file
59
packages/dev-tools-pages/ts/components/ui/button.tsx
Normal 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';
|
55
packages/dev-tools-pages/ts/components/ui/container.tsx
Normal file
55
packages/dev-tools-pages/ts/components/ui/container.tsx
Normal 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';
|
74
packages/dev-tools-pages/ts/components/ui/text.tsx
Normal file
74
packages/dev-tools-pages/ts/components/ui/text.tsx
Normal 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';
|
9
packages/dev-tools-pages/ts/globals.d.ts
vendored
Normal file
9
packages/dev-tools-pages/ts/globals.d.ts
vendored
Normal 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 */
|
||||
}
|
17
packages/dev-tools-pages/ts/index.tsx
Normal file
17
packages/dev-tools-pages/ts/index.tsx
Normal 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'),
|
||||
);
|
27
packages/dev-tools-pages/ts/pages/landing.tsx
Normal file
27
packages/dev-tools-pages/ts/pages/landing.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
32
packages/dev-tools-pages/ts/utils/utils.ts
Normal file
32
packages/dev-tools-pages/ts/utils/utils.ts
Normal 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;
|
||||
},
|
||||
};
|
22
packages/dev-tools-pages/tsconfig.json
Normal file
22
packages/dev-tools-pages/tsconfig.json
Normal 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/**/*"]
|
||||
}
|
10
packages/dev-tools-pages/tslint.json
Normal file
10
packages/dev-tools-pages/tslint.json
Normal 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
|
||||
}
|
||||
}
|
86
packages/dev-tools-pages/webpack.config.js
Normal file
86
packages/dev-tools-pages/webpack.config.js
Normal 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;
|
||||
};
|
@ -1,4 +1,13 @@
|
||||
[
|
||||
{
|
||||
"version": "1.1.0",
|
||||
"changes": [
|
||||
{
|
||||
"note": "Add `JSONRPCResponseError` and error field on `JSONRPCResponsePayload`.",
|
||||
"pr": 1102
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"timestamp": 1538693146,
|
||||
"version": "1.0.11",
|
||||
|
@ -113,10 +113,16 @@ export interface JSONRPCRequestPayload {
|
||||
jsonrpc: string;
|
||||
}
|
||||
|
||||
export interface JSONRPCResponseError {
|
||||
message: string;
|
||||
code: number;
|
||||
}
|
||||
|
||||
export interface JSONRPCResponsePayload {
|
||||
result: any;
|
||||
id: number;
|
||||
jsonrpc: string;
|
||||
error?: JSONRPCResponseError;
|
||||
}
|
||||
|
||||
export interface AbstractBlock {
|
||||
|
@ -45,7 +45,7 @@
|
||||
"@0xproject/utils": "^2.0.2",
|
||||
"@0xproject/web3-wrapper": "^3.0.3",
|
||||
"ethereum-types": "^1.0.11",
|
||||
"ethers": "4.0.0-beta.14",
|
||||
"ethers": "~4.0.4",
|
||||
"lodash": "^4.17.5"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
@ -43,15 +43,19 @@
|
||||
},
|
||||
"homepage": "https://github.com/0xProject/0x-monorepo/packages/instant/README.md",
|
||||
"dependencies": {
|
||||
"@0xproject/connect": "^2.0.4",
|
||||
"@0xproject/asset-buyer": "^2.0.0",
|
||||
"@0xproject/types": "^1.1.4",
|
||||
"@0xproject/typescript-typings": "^2.0.2",
|
||||
"@0xproject/utils": "^1.0.11",
|
||||
"@0xproject/utils": "^2.0.2",
|
||||
"@0xproject/web3-wrapper": "^3.0.3",
|
||||
"ethereum-types": "^1.0.11",
|
||||
"lodash": "^4.17.10",
|
||||
"polished": "^2.2.0",
|
||||
"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": {
|
||||
"@0xproject/tslint-config": "^1.0.8",
|
||||
@ -59,8 +63,10 @@
|
||||
"@types/enzyme-adapter-react-16": "^1.0.3",
|
||||
"@types/lodash": "^4.14.116",
|
||||
"@types/node": "*",
|
||||
"@types/react": "16.4.7",
|
||||
"@types/react": "^16.4.16",
|
||||
"@types/react-dom": "^16.0.8",
|
||||
"@types/react-redux": "^6.0.9",
|
||||
"@types/redux": "^3.6.0",
|
||||
"awesome-typescript-loader": "^5.2.1",
|
||||
"copyfiles": "^1.2.0",
|
||||
"enzyme": "^3.6.0",
|
||||
|
@ -6,6 +6,19 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>0x Instant Dev Environment</title>
|
||||
<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>
|
||||
|
||||
<body>
|
||||
|
47
packages/instant/src/components/amount_input.tsx
Normal file
47
packages/instant/src/components/amount_input.tsx
Normal 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);
|
||||
}
|
||||
};
|
||||
}
|
19
packages/instant/src/components/buy_button.tsx
Normal file
19
packages/instant/src/components/buy_button.tsx
Normal 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';
|
45
packages/instant/src/components/instant_heading.tsx
Normal file
45
packages/instant/src/components/instant_heading.tsx
Normal 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>
|
||||
);
|
62
packages/instant/src/components/order_details.tsx
Normal file
62
packages/instant/src/components/order_details.tsx
Normal 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';
|
60
packages/instant/src/components/ui/button.tsx
Normal file
60
packages/instant/src/components/ui/button.tsx
Normal 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';
|
64
packages/instant/src/components/ui/container.tsx
Normal file
64
packages/instant/src/components/ui/container.tsx
Normal 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';
|
37
packages/instant/src/components/ui/flex.tsx
Normal file
37
packages/instant/src/components/ui/flex.tsx
Normal 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';
|
5
packages/instant/src/components/ui/index.ts
Normal file
5
packages/instant/src/components/ui/index.ts
Normal 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';
|
40
packages/instant/src/components/ui/input.tsx
Normal file
40
packages/instant/src/components/ui/input.tsx
Normal 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';
|
80
packages/instant/src/components/ui/text.tsx
Normal file
80
packages/instant/src/components/ui/text.tsx
Normal 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';
|
@ -1,5 +1,20 @@
|
||||
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 const ZeroExInstant: React.StatelessComponent<ZeroExInstantProps> = () => <div>ZeroExInstant</div>;
|
||||
export const ZeroExInstant: React.StatelessComponent<ZeroExInstantProps> = () => (
|
||||
<Provider store={store}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<ZeroExInstantContainer />
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
@ -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);
|
31
packages/instant/src/redux/reducer.ts
Normal file
31
packages/instant/src/redux/reducer.ts
Normal 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;
|
||||
}
|
||||
};
|
6
packages/instant/src/redux/store.ts
Normal file
6
packages/instant/src/redux/store.ts
Normal 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);
|
10
packages/instant/src/style/fonts.ts
Normal file
10
packages/instant/src/style/fonts.ts
Normal 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');
|
||||
`;
|
||||
},
|
||||
};
|
27
packages/instant/src/style/theme.ts
Normal file
27
packages/instant/src/style/theme.ts
Normal 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 };
|
11
packages/instant/src/style/util.ts
Normal file
11
packages/instant/src/style/util.ts
Normal 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 '';
|
||||
};
|
9
packages/instant/src/types.ts
Normal file
9
packages/instant/src/types.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export enum ActionTypes {
|
||||
UPDATE_ETH_USD_PRICE,
|
||||
UPDATE_SELECTED_ASSET_AMOUNT,
|
||||
}
|
||||
|
||||
export interface Action {
|
||||
type: ActionTypes;
|
||||
data?: any;
|
||||
}
|
@ -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"]
|
||||
}
|
||||
}
|
||||
|
28
packages/json-schemas/schemas/eip712_typed_data.ts
Normal file
28
packages/json-schemas/schemas/eip712_typed_data.ts
Normal 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'],
|
||||
};
|
10
packages/json-schemas/schemas/zero_ex_transaction_schema.ts
Normal file
10
packages/json-schemas/schemas/zero_ex_transaction_schema.ts
Normal 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',
|
||||
};
|
@ -2,6 +2,7 @@ import { addressSchema, hexSchema, numberSchema } from '../schemas/basic_type_sc
|
||||
import { blockParamSchema, blockRangeSchema } from '../schemas/block_range_schema';
|
||||
import { callDataSchema } from '../schemas/call_data_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 { orderCancellationRequestsSchema } from '../schemas/order_cancel_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 { tokenSchema } from '../schemas/token_schema';
|
||||
import { jsNumber, txDataSchema } from '../schemas/tx_data_schema';
|
||||
import { zeroExTransactionSchema } from '../schemas/zero_ex_transaction_schema';
|
||||
|
||||
export const schemas = {
|
||||
numberSchema,
|
||||
@ -39,6 +41,7 @@ export const schemas = {
|
||||
hexSchema,
|
||||
ecSignatureParameterSchema,
|
||||
ecSignatureSchema,
|
||||
eip712TypedDataSchema,
|
||||
indexFilterValuesSchema,
|
||||
orderCancellationRequestsSchema,
|
||||
orderFillOrKillRequestsSchema,
|
||||
@ -68,4 +71,5 @@ export const schemas = {
|
||||
relayerApiOrdersChannelUpdateSchema,
|
||||
relayerApiOrdersResponseSchema,
|
||||
relayerApiAssetDataPairsSchema,
|
||||
zeroExTransactionSchema,
|
||||
};
|
||||
|
@ -41,7 +41,7 @@
|
||||
"@types/mocha": "^5.2.2",
|
||||
"copyfiles": "^2.0.0",
|
||||
"ethereum-types": "^1.0.11",
|
||||
"ethers": "4.0.0-beta.14",
|
||||
"ethers": "~4.0.4",
|
||||
"lodash": "^4.17.5",
|
||||
"run-s": "^0.0.0"
|
||||
},
|
||||
|
@ -54,7 +54,7 @@
|
||||
"@0xproject/web3-wrapper": "^3.0.3",
|
||||
"@ledgerhq/hw-app-eth": "^4.3.0",
|
||||
"ethereum-types": "^1.0.11",
|
||||
"ethers": "4.0.0-beta.14",
|
||||
"ethers": "~4.0.4",
|
||||
"lodash": "^4.17.5"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
@ -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",
|
||||
"changes": [
|
||||
|
@ -70,7 +70,7 @@
|
||||
"ethereum-types": "^1.0.11",
|
||||
"ethereumjs-abi": "0.6.5",
|
||||
"ethereumjs-util": "^5.1.1",
|
||||
"ethers": "4.0.0-beta.14",
|
||||
"ethers": "~4.0.4",
|
||||
"lodash": "^4.17.5"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
@ -13,4 +13,39 @@ export const constants = {
|
||||
BASE_16: 16,
|
||||
INFINITE_TIMESTAMP_SEC: new BigNumber(2524604400), // Close to infinite
|
||||
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' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
@ -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 { crypto } from './crypto';
|
||||
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 },
|
||||
],
|
||||
};
|
||||
import { constants } from './constants';
|
||||
|
||||
export const eip712Utils = {
|
||||
/**
|
||||
* Compiles the EIP712Schema and returns the hash of the schema.
|
||||
* @param schema The EIP712 schema.
|
||||
* @return The hash of the compiled schema
|
||||
* Creates a EIP712TypedData object specific to the 0x protocol for use with signTypedData.
|
||||
* @param primaryType The primary type found in message
|
||||
* @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 {
|
||||
const eip712Schema = eip712Utils._encodeType(schema);
|
||||
const eip712SchemaHashBuffer = crypto.solSHA3([eip712Schema]);
|
||||
return eip712SchemaHashBuffer;
|
||||
createTypedData: (
|
||||
primaryType: string,
|
||||
types: EIP712Types,
|
||||
message: EIP712Object,
|
||||
exchangeAddress: string,
|
||||
): EIP712TypedData => {
|
||||
assert.isETHAddressHex('exchangeAddress', exchangeAddress);
|
||||
assert.isString('primaryType', primaryType);
|
||||
const typedData = {
|
||||
types: {
|
||||
EIP712Domain: constants.EIP712_DOMAIN_SCHEMA.parameters,
|
||||
...types,
|
||||
},
|
||||
/**
|
||||
* Merges the EIP712 hash of a struct with the DomainSeparator for 0x v2.
|
||||
* @param hashStruct the EIP712 hash of a struct
|
||||
* @param contractAddress the exchange contract address
|
||||
* @return The hash of an EIP712 message with domain separator prefixed
|
||||
*/
|
||||
createEIP712Message(hashStruct: Buffer, contractAddress: string): Buffer {
|
||||
const domainSeparatorHashBuffer = eip712Utils._getDomainSeparatorHashBuffer(contractAddress);
|
||||
const messageBuff = crypto.solSHA3([EIP191_PREFIX, domainSeparatorHashBuffer, hashStruct]);
|
||||
return messageBuff;
|
||||
},
|
||||
/**
|
||||
* Pad an address to 32 bytes
|
||||
* @param address Address to pad
|
||||
* @return padded address
|
||||
*/
|
||||
pad32Address(address: string): Buffer {
|
||||
const addressBuffer = ethUtil.toBuffer(address);
|
||||
const addressPadded = eip712Utils.pad32Buffer(addressBuffer);
|
||||
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,
|
||||
domain: {
|
||||
name: constants.EIP712_DOMAIN_NAME,
|
||||
version: constants.EIP712_DOMAIN_VERSION,
|
||||
verifyingContract: exchangeAddress,
|
||||
},
|
||||
message,
|
||||
primaryType,
|
||||
};
|
||||
assert.doesConformToSchema('typedData', typedData, schemas.eip712TypedDataSchema);
|
||||
return typedData;
|
||||
},
|
||||
/**
|
||||
* Creates an Order EIP712TypedData object for use with signTypedData.
|
||||
* @param Order the order
|
||||
* @return A typed data object
|
||||
*/
|
||||
createOrderTypedData: (order: Order): EIP712TypedData => {
|
||||
assert.doesConformToSchema('order', order, schemas.orderSchema, [schemas.hexSchema]);
|
||||
const normalizedOrder = _.mapValues(order, value => {
|
||||
return !_.isString(value) ? value.toString() : value;
|
||||
});
|
||||
const domainSeparatorHashBuff2 = crypto.solSHA3([domainSeparatorSchemaBuffer, ...encodedData]);
|
||||
return domainSeparatorHashBuff2;
|
||||
const typedData = eip712Utils.createTypedData(
|
||||
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}`);
|
||||
const namedTypesJoined = namedTypes.join(',');
|
||||
const encodedType = `${schema.name}(${namedTypesJoined})`;
|
||||
return encodedType;
|
||||
},
|
||||
_encodeData(schema: EIP712Schema, data: { [key: string]: any }): any {
|
||||
const encodedValues = [];
|
||||
for (const parameter of schema.parameters) {
|
||||
const value = data[parameter.name];
|
||||
if (parameter.type === EIP712Types.String || parameter.type === EIP712Types.Bytes) {
|
||||
encodedValues.push(crypto.solSHA3([ethUtil.toBuffer(value)]));
|
||||
} else if (parameter.type === EIP712Types.Uint256) {
|
||||
encodedValues.push(value);
|
||||
} else if (parameter.type === EIP712Types.Address) {
|
||||
encodedValues.push(eip712Utils.pad32Address(value));
|
||||
} else {
|
||||
throw new Error(`Unable to encode ${parameter.type}`);
|
||||
}
|
||||
}
|
||||
return encodedValues;
|
||||
/**
|
||||
* Creates an ExecuteTransaction EIP712TypedData object for use with signTypedData and
|
||||
* 0x Exchange executeTransaction.
|
||||
* @param ZeroExTransaction the 0x transaction
|
||||
* @param exchangeAddress The address of the exchange contract
|
||||
* @return A typed data object
|
||||
*/
|
||||
createZeroExTransactionTypedData: (
|
||||
zeroExTransaction: ZeroExTransaction,
|
||||
exchangeAddress: string,
|
||||
): EIP712TypedData => {
|
||||
assert.isETHAddressHex('exchangeAddress', exchangeAddress);
|
||||
assert.doesConformToSchema('zeroExTransaction', zeroExTransaction, schemas.zeroExTransactionSchema);
|
||||
const normalizedTransaction = _.mapValues(zeroExTransaction, value => {
|
||||
return !_.isString(value) ? value.toString() : value;
|
||||
});
|
||||
const typedData = eip712Utils.createTypedData(
|
||||
constants.EIP712_ZEROEX_TRANSACTION_SCHEMA.name,
|
||||
{ ZeroExTransaction: constants.EIP712_ZEROEX_TRANSACTION_SCHEMA.parameters },
|
||||
normalizedTransaction,
|
||||
exchangeAddress,
|
||||
);
|
||||
return typedData;
|
||||
},
|
||||
};
|
||||
|
@ -2,7 +2,6 @@ export { orderHashUtils } from './order_hash';
|
||||
export { signatureUtils } from './signature_utils';
|
||||
export { generatePseudoRandomSalt } from './salt';
|
||||
export { assetDataUtils } from './asset_data_utils';
|
||||
export { eip712Utils } from './eip712_utils';
|
||||
export { marketUtils } from './market_utils';
|
||||
export { rateUtils } from './rate_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 { 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 {
|
||||
SignedOrder,
|
||||
Order,
|
||||
@ -29,17 +38,19 @@ export {
|
||||
ERC20AssetData,
|
||||
ERC721AssetData,
|
||||
AssetProxyId,
|
||||
SignerType,
|
||||
SignatureType,
|
||||
OrderStateValid,
|
||||
OrderStateInvalid,
|
||||
ExchangeContractErrs,
|
||||
EIP712Parameter,
|
||||
EIP712TypedData,
|
||||
EIP712Types,
|
||||
EIP712Object,
|
||||
EIP712ObjectValue,
|
||||
ZeroExTransaction,
|
||||
} from '@0xproject/types';
|
||||
export {
|
||||
OrderError,
|
||||
EIP712Parameter,
|
||||
EIP712Schema,
|
||||
EIP712Types,
|
||||
TradeSide,
|
||||
TransferType,
|
||||
FindFeeOrdersThatCoverFeesForTargetOrdersOpts,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Order, SignedOrder, SignerType } from '@0xproject/types';
|
||||
import { Order, SignedOrder } from '@0xproject/types';
|
||||
import { BigNumber } from '@0xproject/utils';
|
||||
import { Provider } from 'ethereum-types';
|
||||
import * as _ from 'lodash';
|
||||
@ -71,12 +71,7 @@ export const orderFactory = {
|
||||
createOrderOpts,
|
||||
);
|
||||
const orderHash = orderHashUtils.getOrderHashHex(order);
|
||||
const signature = await signatureUtils.ecSignOrderHashAsync(
|
||||
provider,
|
||||
orderHash,
|
||||
makerAddress,
|
||||
SignerType.Default,
|
||||
);
|
||||
const signature = await signatureUtils.ecSignHashAsync(provider, orderHash, makerAddress);
|
||||
const signedOrder: SignedOrder = _.assign(order, { signature });
|
||||
return signedOrder;
|
||||
},
|
||||
|
@ -1,31 +1,13 @@
|
||||
import { schemas, SchemaValidator } from '@0xproject/json-schemas';
|
||||
import { Order, SignedOrder } from '@0xproject/types';
|
||||
import { signTypedDataUtils } from '@0xproject/utils';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { assert } from './assert';
|
||||
import { eip712Utils } from './eip712_utils';
|
||||
import { EIP712Schema, EIP712Types } from './types';
|
||||
|
||||
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 = {
|
||||
/**
|
||||
* Checks if the supplied hex encoded order hash is valid.
|
||||
@ -45,7 +27,7 @@ export const orderHashUtils = {
|
||||
/**
|
||||
* Computes the orderHash for a supplied order.
|
||||
* @param order An object that conforms to the Order or SignedOrder interface definitions.
|
||||
* @return The resulting orderHash from hashing the supplied order.
|
||||
* @return Hex encoded string orderHash from hashing the supplied order.
|
||||
*/
|
||||
getOrderHashHex(order: SignedOrder | Order): string {
|
||||
try {
|
||||
@ -64,16 +46,13 @@ export const orderHashUtils = {
|
||||
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.
|
||||
* @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 {
|
||||
const orderParamsHashBuff = eip712Utils.structHash(EIP712_ORDER_SCHEMA, order);
|
||||
const orderHashBuff = eip712Utils.createEIP712Message(orderParamsHashBuff, order.exchangeAddress);
|
||||
const typedData = eip712Utils.createOrderTypedData(order);
|
||||
const orderHashBuff = signTypedDataUtils.generateTypedDataHash(typedData);
|
||||
return orderHashBuff;
|
||||
},
|
||||
_getOrderSchemaBuffer(): Buffer {
|
||||
return eip712Utils.compileSchema(EIP712_ORDER_SCHEMA);
|
||||
},
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { Provider } from 'ethereum-types';
|
||||
import * as ethUtil from 'ethereumjs-util';
|
||||
@ -7,9 +7,11 @@ import * as _ from 'lodash';
|
||||
|
||||
import { artifacts } from './artifacts';
|
||||
import { assert } from './assert';
|
||||
import { eip712Utils } from './eip712_utils';
|
||||
import { ExchangeContract } from './generated_contract_wrappers/exchange';
|
||||
import { IValidatorContract } from './generated_contract_wrappers/i_validator';
|
||||
import { IWalletContract } from './generated_contract_wrappers/i_wallet';
|
||||
import { orderHashUtils } from './order_hash';
|
||||
import { OrderError } from './types';
|
||||
import { utils } from './utils';
|
||||
|
||||
@ -49,7 +51,7 @@ export const signatureUtils = {
|
||||
|
||||
case SignatureType.EthSign: {
|
||||
const ecSignature = signatureUtils.parseECSignature(signature);
|
||||
const prefixedMessageHex = signatureUtils.addSignedMessagePrefix(data, SignerType.Default);
|
||||
const prefixedMessageHex = signatureUtils.addSignedMessagePrefix(data);
|
||||
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.
|
||||
* This method currently supports TestRPC, Geth and Parity above and below V1.6.6
|
||||
* @param orderHash Hex encoded orderHash to sign.
|
||||
* Signs an order and returns a SignedOrder. First `eth_signTypedData` is requested
|
||||
* then a fallback to `eth_sign` if not available on the supplied provider.
|
||||
* @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 Provider supplied to 0x.js.
|
||||
* @param signerType Different signers add/require different prefixes to be prepended to the message being signed.
|
||||
* Since we cannot know ahead of time which signer you are using, you must supply a SignerType.
|
||||
* @return A hex encoded string containing the Elliptic curve signature generated by signing the orderHash and the Signature Type.
|
||||
* must be available via the supplied Provider.
|
||||
* @return A SignedOrder containing the order and Elliptic curve signature with Signature Type.
|
||||
*/
|
||||
async ecSignOrderHashAsync(
|
||||
provider: Provider,
|
||||
orderHash: string,
|
||||
signerAddress: string,
|
||||
signerType: SignerType,
|
||||
): Promise<string> {
|
||||
async ecSignOrderAsync(provider: Provider, order: Order, signerAddress: string): Promise<SignedOrder> {
|
||||
assert.doesConformToSchema('order', order, schemas.orderSchema, [schemas.hexSchema]);
|
||||
try {
|
||||
const signedOrder = await signatureUtils.ecSignTypedDataOrderAsync(provider, order, signerAddress);
|
||||
return signedOrder;
|
||||
} 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.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);
|
||||
const web3Wrapper = new Web3Wrapper(provider);
|
||||
await assert.isSenderAddressAsync('signerAddress', signerAddress, web3Wrapper);
|
||||
const normalizedSignerAddress = signerAddress.toLowerCase();
|
||||
|
||||
let msgHashHex = orderHash;
|
||||
const prefixedMsgHashHex = signatureUtils.addSignedMessagePrefix(orderHash, signerType);
|
||||
// Metamask incorrectly implements eth_sign and does not prefix the message as per the spec
|
||||
// Source: https://github.com/MetaMask/metamask-extension/commit/a9d36860bec424dcee8db043d3e7da6a5ff5672e
|
||||
if (signerType === SignerType.Metamask) {
|
||||
msgHashHex = prefixedMsgHashHex;
|
||||
}
|
||||
const signature = await web3Wrapper.signMessageAsync(normalizedSignerAddress, msgHashHex);
|
||||
const signature = await web3Wrapper.signMessageAsync(normalizedSignerAddress, msgHash);
|
||||
const prefixedMsgHashHex = signatureUtils.addSignedMessagePrefix(msgHash);
|
||||
|
||||
// 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)
|
||||
@ -238,10 +294,7 @@ export const signatureUtils = {
|
||||
normalizedSignerAddress,
|
||||
);
|
||||
if (isValidRSVSignature) {
|
||||
const convertedSignatureHex = signatureUtils.convertECSignatureToSignatureHex(
|
||||
ecSignatureRSV,
|
||||
signerType,
|
||||
);
|
||||
const convertedSignatureHex = signatureUtils.convertECSignatureToSignatureHex(ecSignatureRSV);
|
||||
return convertedSignatureHex;
|
||||
}
|
||||
}
|
||||
@ -253,41 +306,30 @@ export const signatureUtils = {
|
||||
normalizedSignerAddress,
|
||||
);
|
||||
if (isValidVRSSignature) {
|
||||
const convertedSignatureHex = signatureUtils.convertECSignatureToSignatureHex(
|
||||
ecSignatureVRS,
|
||||
signerType,
|
||||
);
|
||||
const convertedSignatureHex = signatureUtils.convertECSignatureToSignatureHex(ecSignatureVRS);
|
||||
return convertedSignatureHex;
|
||||
}
|
||||
}
|
||||
|
||||
// Detect if Metamask to transition users to the MetamaskSubprovider
|
||||
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 signerType The SignerType of the signed data
|
||||
* @return Hex encoded string of signature (v,r,s) with Signature Type
|
||||
*/
|
||||
convertECSignatureToSignatureHex(ecSignature: ECSignature, signerType: SignerType): string {
|
||||
convertECSignatureToSignatureHex(ecSignature: ECSignature): string {
|
||||
const signatureBuffer = Buffer.concat([
|
||||
ethUtil.toBuffer(ecSignature.v),
|
||||
ethUtil.toBuffer(ecSignature.r),
|
||||
ethUtil.toBuffer(ecSignature.s),
|
||||
]);
|
||||
const signatureHex = `0x${signatureBuffer.toString('hex')}`;
|
||||
let signatureType;
|
||||
switch (signerType) {
|
||||
case SignerType.Metamask:
|
||||
case SignerType.Ledger:
|
||||
case SignerType.Default: {
|
||||
signatureType = SignatureType.EthSign;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unrecognized SignerType: ${signerType}`);
|
||||
}
|
||||
const signatureWithType = signatureUtils.convertToSignatureWithType(signatureHex, signatureType);
|
||||
const signatureWithType = signatureUtils.convertToSignatureWithType(signatureHex, SignatureType.EthSign);
|
||||
return signatureWithType;
|
||||
},
|
||||
/**
|
||||
@ -304,28 +346,17 @@ export const signatureUtils = {
|
||||
/**
|
||||
* Adds the relevant prefix to the message being signed.
|
||||
* @param message Message to sign
|
||||
* @param signerType The type of message prefix to add for a given SignerType. Different signers expect
|
||||
* specific message prefixes.
|
||||
* @return Prefixed message
|
||||
*/
|
||||
addSignedMessagePrefix(message: string, signerType: SignerType = SignerType.Default): string {
|
||||
addSignedMessagePrefix(message: string): string {
|
||||
assert.isString('message', message);
|
||||
assert.doesBelongToStringEnum('signerType', signerType, SignerType);
|
||||
switch (signerType) {
|
||||
case SignerType.Metamask:
|
||||
case SignerType.Ledger:
|
||||
case SignerType.Default: {
|
||||
const msgBuff = ethUtil.toBuffer(message);
|
||||
const prefixedMsgBuff = ethUtil.hashPersonalMessage(msgBuff);
|
||||
const prefixedMsgHex = ethUtil.bufferToHex(prefixedMsgBuff);
|
||||
return prefixedMsgHex;
|
||||
}
|
||||
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
|
||||
* @return An ECSignature object with r,s,v parameters
|
||||
*/
|
||||
|
@ -2,6 +2,7 @@ import { BigNumber } from '@0xproject/utils';
|
||||
|
||||
export enum OrderError {
|
||||
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 {
|
||||
@ -14,24 +15,6 @@ export enum TransferType {
|
||||
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 {
|
||||
takerAddress?: string;
|
||||
senderAddress?: string;
|
||||
|
44
packages/order-utils/test/eip712_utils_test.ts
Normal file
44
packages/order-utils/test/eip712_utils_test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
@ -35,6 +35,20 @@ describe('Order hashing', () => {
|
||||
const orderHash = orderHashUtils.getOrderHashHex(order);
|
||||
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 () => {
|
||||
const orderWithInvalidtakerFormat = {
|
||||
...order,
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { SignerType } from '@0xproject/types';
|
||||
import { Order, SignatureType } from '@0xproject/types';
|
||||
import { BigNumber } from '@0xproject/utils';
|
||||
import * as chai from 'chai';
|
||||
import { JSONRPCErrorCallback, JSONRPCRequestPayload } from 'ethereum-types';
|
||||
import * as ethUtil from 'ethereumjs-util';
|
||||
import * as _ from 'lodash';
|
||||
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 { chaiSetup } from './utils/chai_setup';
|
||||
@ -16,6 +17,28 @@ chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
|
||||
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', () => {
|
||||
let dataHex = '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0';
|
||||
const ethSignSignature =
|
||||
@ -115,28 +138,64 @@ describe('Signature utils', () => {
|
||||
expect(salt.lessThan(twoPow256)).to.be.true();
|
||||
});
|
||||
});
|
||||
describe('#ecSignOrderHashAsync', () => {
|
||||
let stubs: Sinon.SinonStub[] = [];
|
||||
let makerAddress: string;
|
||||
describe('#ecSignOrderAsync', () => {
|
||||
it('should default to eth_sign if eth_signTypedData is unavailable', async () => {
|
||||
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 () => {
|
||||
const availableAddreses = await web3Wrapper.getAvailableAddressesAsync();
|
||||
makerAddress = availableAddreses[0];
|
||||
});
|
||||
afterEach(() => {
|
||||
// clean up any stubs after the test has completed
|
||||
_.each(stubs, s => s.restore());
|
||||
stubs = [];
|
||||
});
|
||||
it('Should return the correct Signature', async () => {
|
||||
it('should return the correct Signature', async () => {
|
||||
const orderHash = '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0';
|
||||
const expectedSignature =
|
||||
'0x1b61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc3340349190569279751135161d22529dc25add4f6069af05be04cacbda2ace225403';
|
||||
const ecSignature = await signatureUtils.ecSignOrderHashAsync(
|
||||
provider,
|
||||
orderHash,
|
||||
makerAddress,
|
||||
SignerType.Default,
|
||||
);
|
||||
const ecSignature = await signatureUtils.ecSignHashAsync(provider, orderHash, makerAddress);
|
||||
expect(ecSignature).to.equal(expectedSignature);
|
||||
});
|
||||
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(
|
||||
fakeProvider,
|
||||
orderHash,
|
||||
makerAddress,
|
||||
SignerType.Default,
|
||||
);
|
||||
const ecSignature = await signatureUtils.ecSignHashAsync(fakeProvider, orderHash, makerAddress);
|
||||
expect(ecSignature).to.equal(expectedSignature);
|
||||
});
|
||||
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(
|
||||
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,
|
||||
);
|
||||
const ecSignature = await signatureUtils.ecSignHashAsync(fakeProvider, orderHash, makerAddress);
|
||||
expect(ecSignature).to.equal(expectedSignature);
|
||||
});
|
||||
it('should return a valid signature', async () => {
|
||||
const orderHash = '0x34decbedc118904df65f379a175bb39ca18209d6ce41d5ed549d54e6e0a95004';
|
||||
const ecSignature = await signatureUtils.ecSignOrderHashAsync(
|
||||
provider,
|
||||
orderHash,
|
||||
makerAddress,
|
||||
SignerType.Default,
|
||||
);
|
||||
const ecSignature = await signatureUtils.ecSignHashAsync(provider, orderHash, makerAddress);
|
||||
|
||||
const isValidSignature = await signatureUtils.isValidSignatureAsync(
|
||||
provider,
|
||||
@ -250,44 +260,65 @@ describe('Signature utils', () => {
|
||||
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', () => {
|
||||
const ecSignature: ECSignature = {
|
||||
v: 27,
|
||||
r: '0xaca7da997ad177f040240cdccf6905b71ab16b74434388c3a72f34fd25d64393',
|
||||
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 =
|
||||
'0x1baca7da997ad177f040240cdccf6905b71ab16b74434388c3a72f34fd25d6439346b2bac274ff29b48b3ea6e2d04c1336eaceafda3c53ab483fc3ff12fac3ebf203';
|
||||
const signatureWithSignatureType = signatureUtils.convertECSignatureToSignatureHex(
|
||||
ecSignature,
|
||||
SignerType.Default,
|
||||
);
|
||||
const signatureWithSignatureType = signatureUtils.convertECSignatureToSignatureHex(ecSignature);
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -82,7 +82,7 @@
|
||||
"bintrees": "^1.0.2",
|
||||
"ethereum-types": "^1.0.11",
|
||||
"ethereumjs-blockstream": "6.0.0",
|
||||
"ethers": "4.0.0-beta.14",
|
||||
"ethers": "~4.0.4",
|
||||
"lodash": "^4.17.5"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
@ -13,4 +13,10 @@ export {
|
||||
export { OnOrderStateChangeCallback, OrderWatcherConfig } from './types';
|
||||
|
||||
export { SignedOrder } from '@0xproject/types';
|
||||
export { JSONRPCRequestPayload, JSONRPCErrorCallback, Provider, JSONRPCResponsePayload } from 'ethereum-types';
|
||||
export {
|
||||
JSONRPCRequestPayload,
|
||||
JSONRPCErrorCallback,
|
||||
Provider,
|
||||
JSONRPCResponsePayload,
|
||||
JSONRPCResponseError,
|
||||
} from 'ethereum-types';
|
||||
|
@ -8,7 +8,13 @@ export { ProfilerSubprovider } from './profiler_subprovider';
|
||||
export { RevertTraceSubprovider } from './revert_trace_subprovider';
|
||||
|
||||
export { ContractData } from './types';
|
||||
export { JSONRPCRequestPayload, Provider, JSONRPCErrorCallback, JSONRPCResponsePayload } from 'ethereum-types';
|
||||
export {
|
||||
JSONRPCRequestPayload,
|
||||
Provider,
|
||||
JSONRPCErrorCallback,
|
||||
JSONRPCResponsePayload,
|
||||
JSONRPCResponseError,
|
||||
} from 'ethereum-types';
|
||||
|
||||
export {
|
||||
JSONRPCRequestPayloadWithMethod,
|
||||
|
@ -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",
|
||||
"changes": [
|
||||
|
@ -45,7 +45,7 @@
|
||||
"ethereum-types": "^1.0.11",
|
||||
"ethereumjs-tx": "^1.3.5",
|
||||
"ethereumjs-util": "^5.1.1",
|
||||
"ganache-core": "0xProject/ganache-core#monorepo-dep",
|
||||
"ganache-core": "^2.2.1",
|
||||
"hdkey": "^0.7.1",
|
||||
"json-rpc-error": "2.0.0",
|
||||
"lodash": "^4.17.5",
|
||||
|
@ -27,6 +27,7 @@ export { Subprovider } from './subproviders/subprovider';
|
||||
export { NonceTrackerSubprovider } from './subproviders/nonce_tracker';
|
||||
export { PrivateKeyWalletSubprovider } from './subproviders/private_key_wallet';
|
||||
export { MnemonicWalletSubprovider } from './subproviders/mnemonic_wallet';
|
||||
export { MetamaskSubprovider } from './subproviders/metamask_subprovider';
|
||||
export { EthLightwalletSubprovider } from './subproviders/eth_lightwallet_subprovider';
|
||||
|
||||
export {
|
||||
@ -47,6 +48,19 @@ export {
|
||||
LedgerGetAddressResult,
|
||||
} 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';
|
||||
|
@ -23,6 +23,7 @@ export abstract class BaseWalletSubprovider extends Subprovider {
|
||||
public abstract async getAccountsAsync(): Promise<string[]>;
|
||||
public abstract async signTransactionAsync(txParams: PartialTxParams): 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.
|
||||
@ -36,6 +37,8 @@ export abstract class BaseWalletSubprovider extends Subprovider {
|
||||
public async handleRequest(payload: JSONRPCRequestPayload, next: Callback, end: ErrorCallback): Promise<void> {
|
||||
let accounts;
|
||||
let txParams;
|
||||
let address;
|
||||
let typedData;
|
||||
switch (payload.method) {
|
||||
case 'eth_coinbase':
|
||||
try {
|
||||
@ -86,7 +89,7 @@ export abstract class BaseWalletSubprovider extends Subprovider {
|
||||
case 'eth_sign':
|
||||
case 'personal_sign':
|
||||
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 {
|
||||
const ecSignatureHex = await this.signPersonalMessageAsync(data, address);
|
||||
end(null, ecSignatureHex);
|
||||
@ -94,6 +97,15 @@ export abstract class BaseWalletSubprovider extends Subprovider {
|
||||
end(err);
|
||||
}
|
||||
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:
|
||||
next();
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { EIP712TypedData } from '@0xproject/types';
|
||||
import * as lightwallet from 'eth-lightwallet';
|
||||
|
||||
import { PartialTxParams } from '../types';
|
||||
@ -48,16 +49,16 @@ export class EthLightwalletSubprovider extends BaseWalletSubprovider {
|
||||
// Lightwallet loses the chain id information when hex encoding the transaction
|
||||
// this results in a different signature on certain networks. PrivateKeyWallet
|
||||
// respects this as it uses the parameters passed in
|
||||
let privKey = this._keystore.exportPrivateKey(txParams.from, this._pwDerivedKey);
|
||||
const privKeyWallet = new PrivateKeyWalletSubprovider(privKey);
|
||||
privKey = '';
|
||||
const privKeySignature = await privKeyWallet.signTransactionAsync(txParams);
|
||||
return privKeySignature;
|
||||
let privateKey = this._keystore.exportPrivateKey(txParams.from, this._pwDerivedKey);
|
||||
const privateKeyWallet = new PrivateKeyWalletSubprovider(privateKey);
|
||||
privateKey = '';
|
||||
const privateKeySignature = await privateKeyWallet.signTransactionAsync(txParams);
|
||||
return privateKeySignature;
|
||||
}
|
||||
/**
|
||||
* Sign a personal Ethereum signed message. The signing account will be the account
|
||||
* 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.
|
||||
* If you are not using this via a ProviderEngine instance, you can call it directly.
|
||||
* @param data Hex string message to sign
|
||||
@ -65,10 +66,26 @@ export class EthLightwalletSubprovider extends BaseWalletSubprovider {
|
||||
* @return Signature hex string (order: rsv)
|
||||
*/
|
||||
public async signPersonalMessageAsync(data: string, address: string): Promise<string> {
|
||||
let privKey = this._keystore.exportPrivateKey(address, this._pwDerivedKey);
|
||||
const privKeyWallet = new PrivateKeyWalletSubprovider(privKey);
|
||||
privKey = '';
|
||||
const result = privKeyWallet.signPersonalMessageAsync(data, address);
|
||||
let privateKey = this._keystore.exportPrivateKey(address, this._pwDerivedKey);
|
||||
const privateKeyWallet = new PrivateKeyWalletSubprovider(privateKey);
|
||||
privateKey = '';
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -187,6 +187,16 @@ export class LedgerSubprovider extends BaseWalletSubprovider {
|
||||
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> {
|
||||
await this._connectionLock.acquire();
|
||||
if (!_.isUndefined(this._ledgerClientIfExists)) {
|
||||
|
126
packages/subproviders/src/subproviders/metamask_subprovider.ts
Normal file
126
packages/subproviders/src/subproviders/metamask_subprovider.ts
Normal 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 });
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import { assert } from '@0xproject/assert';
|
||||
import { EIP712TypedData } from '@0xproject/types';
|
||||
import { addressUtils } from '@0xproject/utils';
|
||||
import * as bip39 from 'bip39';
|
||||
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
|
||||
* associated with the provided address.
|
||||
* If you've added the MnemonicWalletSubprovider 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.
|
||||
* If you are not using this via a ProviderEngine instance, you can call it directly.
|
||||
* associated with the provided address. If you've added the MnemonicWalletSubprovider 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. If you are not using this via a ProviderEngine
|
||||
* instance, you can call it directly.
|
||||
* @param data Hex string message to sign
|
||||
* @param address Address of the account to sign with
|
||||
* @return Signature hex string (order: rsv)
|
||||
@ -108,6 +109,25 @@ export class MnemonicWalletSubprovider extends BaseWalletSubprovider {
|
||||
const sig = await privateKeyWallet.signPersonalMessageAsync(data, address);
|
||||
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 {
|
||||
const derivedKeyInfo = this._findDerivedKeyInfoForAddress(address);
|
||||
const privateKeyHex = derivedKeyInfo.hdKey.privateKey.toString('hex');
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { assert } from '@0xproject/assert';
|
||||
import { EIP712TypedData } from '@0xproject/types';
|
||||
import { signTypedDataUtils } from '@0xproject/utils';
|
||||
import EthereumTx = require('ethereumjs-tx');
|
||||
import * as ethUtil from 'ethereumjs-util';
|
||||
import * as _ from 'lodash';
|
||||
@ -23,7 +25,7 @@ export class PrivateKeyWalletSubprovider extends BaseWalletSubprovider {
|
||||
constructor(privateKey: string) {
|
||||
assert.isString('privateKey', privateKey);
|
||||
super();
|
||||
this._privateKeyBuffer = new Buffer(privateKey, 'hex');
|
||||
this._privateKeyBuffer = Buffer.from(privateKey, '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);
|
||||
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
Loading…
x
Reference in New Issue
Block a user