Merge branch 'development' into breakUp0xjs

* development: (38 commits)
  Add fallback image support to relayer grid tile
  Clear relayer grid state when fetching
  Configure the compiler to generate artifacts with deployedBytecode
  Implement loading and error state for relayer grid
  Fallback image for relayer grid tile
  Change relayer grid tile to link on header
  Display top tokens from backend
  Remove overflowZ property from portal
  Suggestions and fix bad merge
  Fix typo
  Only show untracked tokens
  Make wallet scrollable
  Add token flow
  Update The Ocean logo
  Fix artifacts paths
  Create an artifacts folder
  Introduce a var
  Add removeHexPrefix util method
  CHeck if ABI exists
  Improve the readability of the check for should compile
  ...

# Conflicts:
#	.gitignore
#	packages/contracts/test/multi_sig_with_time_lock.ts
#	packages/contracts/test/multi_sig_with_time_lock_except_remove_auth_addr.ts
#	packages/contracts/util/artifacts.ts
This commit is contained in:
Fabio Berger 2018-05-10 17:08:07 +02:00
commit cd5f00ac4d
46 changed files with 986 additions and 512 deletions

2
.gitignore vendored
View File

@ -79,7 +79,7 @@ packages/metacoin/artifacts
packages/0x.js/test/artifacts
packages/order-watcher/test/artifacts
packages/contract-wrappers/test/artifacts
packages/migrations/src/artifacts
packages/migrations/artifacts
# generated contract wrappers
packages/0x.js/src/contract_wrappers/generated/

View File

@ -2,6 +2,6 @@ lib
.nyc_output
/packages/contracts/src/artifacts
/packages/metacoin/artifacts
/packages/migrations/src/artifacts
/packages/migrations/artifacts/1.0.0
package.json
scripts/postpublish_utils.js

View File

@ -21,7 +21,7 @@
"test": "run-s clean test:commonjs",
"test:coverage": "nyc npm run test --all && yarn coverage:report:lcov",
"coverage:report:lcov": "nyc report --reporter=text-lcov > coverage/lcov.info",
"update_artifacts": "for i in ${npm_package_config_contracts}; do copyfiles -u 4 ../migrations/src/artifacts/$i.json test/artifacts; done;",
"update_artifacts": "for i in ${npm_package_config_contracts}; do copyfiles -u 4 ../migrations/artifacts/1.0.0/$i.json test/artifacts; done;",
"clean": "shx rm -rf _bundles lib test_temp scripts",
"build:umd:prod": "NODE_ENV=production webpack",
"build:commonjs": "tsc && yarn update_artifacts && copyfiles -u 2 './src/compact_artifacts/**/*.json' ./lib/src/compact_artifacts && copyfiles -u 3 './lib/src/monorepo_scripts/**/*' ./scripts",

View File

@ -108,8 +108,8 @@ for (const abiFileName of abiFileNames) {
ABI = parsedContent; // ABI file
} else if (!_.isUndefined(parsedContent.abi)) {
ABI = parsedContent.abi; // Truffle artifact
} else if (!_.isUndefined(parsedContent.networks) && !_.isUndefined(parsedContent.networks[args.networkId])) {
ABI = parsedContent.networks[args.networkId].abi; // 0x contracts package artifact
} else if (!_.isUndefined(parsedContent.compilerOutput.abi)) {
ABI = parsedContent.compilerOutput.abi; // 0x artifact
}
if (_.isUndefined(ABI)) {
logUtils.log(`${chalk.red(`ABI not found in ${abiFileName}.`)}`);

View File

@ -0,0 +1,33 @@
{
"artifactsDir": "../migrations/artifacts/1.0.0",
"contractsDir": "src/contracts",
"compilerSettings": {
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode.object",
"evm.bytecode.sourceMap",
"evm.deployedBytecode.object",
"evm.deployedBytecode.sourceMap"
]
}
}
},
"contracts": [
"Exchange",
"DummyToken",
"ZRXToken",
"Token",
"WETH9",
"TokenTransferProxy",
"MultiSigWallet",
"MultiSigWalletWithTimeLock",
"MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress",
"MaliciousToken",
"TokenRegistry",
"Arbitrage",
"EtherDelta",
"AccountLevels"
]
}

View File

@ -10,13 +10,13 @@
"scripts": {
"build:watch": "tsc -w",
"prebuild": "run-s clean compile copy_artifacts generate_contract_wrappers",
"copy_artifacts": "copyfiles -u 4 '../migrations/src/artifacts/**/*' ./lib/src/artifacts;",
"copy_artifacts": "copyfiles -u 4 '../migrations/artifacts/1.0.0/**/*' ./lib/src/artifacts;",
"build": "tsc",
"test": "run-s build run_mocha",
"test:coverage": "SOLIDITY_COVERAGE=true run-s build run_mocha coverage:report:text coverage:report:lcov",
"run_mocha": "mocha 'lib/test/**/*.js' --timeout 100000 --bail --exit",
"compile:comment": "Yarn workspaces do not link binaries correctly so we need to reference them directly https://github.com/yarnpkg/yarn/issues/3846",
"compile": "node ../deployer/lib/src/cli.js compile --contracts ${npm_package_config_contracts} --contracts-dir src/contracts --artifacts-dir ../migrations/src/artifacts",
"compile": "node ../deployer/lib/src/cli.js compile",
"clean": "shx rm -rf ./lib",
"generate_contract_wrappers": "node ../abi-gen/lib/index.js --abis ${npm_package_config_abis} --template ../contract_templates/contract.handlebars --partials '../contract_templates/partials/**/*.handlebars' --output src/contract_wrappers/generated --backend ethers && prettier --write 'src/contract_wrappers/generated/**.ts'",
"lint": "tslint --project . 'migrations/**/*.ts' 'test/**/*.ts' 'util/**/*.ts' 'deploy/**/*.ts'",
@ -26,8 +26,7 @@
"test:circleci": "yarn test:coverage"
},
"config": {
"abis": "../migrations/src/artifacts/@(DummyToken|TokenTransferProxy|Exchange|TokenRegistry|MultiSigWallet|MultiSigWalletWithTimeLock|MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress|TokenRegistry|ZRXToken|Arbitrage|EtherDelta|AccountLevels).json",
"contracts": "Exchange,DummyToken,ZRXToken,Token,WETH9,TokenTransferProxy,MultiSigWallet,MultiSigWalletWithTimeLock,MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress,MaliciousToken,TokenRegistry,Arbitrage,EtherDelta,AccountLevels"
"abis": "../migrations/artifacts/1.0.0/@(DummyToken|TokenTransferProxy|Exchange|TokenRegistry|MultiSigWallet|MultiSigWalletWithTimeLock|MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress|TokenRegistry|ZRXToken|Arbitrage|EtherDelta|AccountLevels).json"
},
"repository": {
"type": "git",

View File

@ -17,7 +17,7 @@ import { chaiSetup } from './utils/chai_setup';
import { deployer } from './utils/deployer';
import { provider, web3Wrapper } from './utils/web3_wrapper';
const MULTI_SIG_ABI = artifacts.MultiSigWalletWithTimeLock.networks[constants.TESTRPC_NETWORK_ID].abi;
const MULTI_SIG_ABI = artifacts.MultiSigWalletWithTimeLock.compilerOutput.abi;
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);

View File

@ -17,10 +17,9 @@ import { ContractName, SubmissionContractEventArgs, TransactionDataParams } from
import { chaiSetup } from './utils/chai_setup';
import { deployer } from './utils/deployer';
import { provider, web3Wrapper } from './utils/web3_wrapper';
const PROXY_ABI = artifacts.TokenTransferProxy.networks[constants.TESTRPC_NETWORK_ID].abi;
const PROXY_ABI = artifacts.TokenTransferProxy.compilerOutput.abi;
const MUTISIG_WALLET_WITH_TIME_LOCK_EXCEPT_REMOVE_AUTHORIZED_ADDRESS_ABI =
artifacts.MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress.networks[constants.TESTRPC_NETWORK_ID]
.abi;
artifacts.MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress.compilerOutput.abi;
chaiSetup.configure();
const expect = chai.expect;

View File

@ -1,3 +1,5 @@
import { ContractArtifact } from '@0xproject/deployer';
import * as DummyToken from '../src/artifacts/DummyToken.json';
import * as Exchange from '../src/artifacts/Exchange.json';
import * as MaliciousToken from '../src/artifacts/MaliciousToken.json';
@ -9,17 +11,15 @@ import * as TokenTransferProxy from '../src/artifacts/TokenTransferProxy.json';
import * as EtherToken from '../src/artifacts/WETH9.json';
import * as ZRX from '../src/artifacts/ZRXToken.json';
import { Artifact } from './types';
export const artifacts = {
ZRX: (ZRX as any) as Artifact,
DummyToken: (DummyToken as any) as Artifact,
Token: (Token as any) as Artifact,
Exchange: (Exchange as any) as Artifact,
EtherToken: (EtherToken as any) as Artifact,
TokenRegistry: (TokenRegistry as any) as Artifact,
MaliciousToken: (MaliciousToken as any) as Artifact,
TokenTransferProxy: (TokenTransferProxy as any) as Artifact,
MultiSigWalletWithTimeLock: (MultiSigWalletWithTimeLock as any) as Artifact,
MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress: (MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress as any) as Artifact,
ZRX: (ZRX as any) as ContractArtifact,
DummyToken: (DummyToken as any) as ContractArtifact,
Token: (Token as any) as ContractArtifact,
Exchange: (Exchange as any) as ContractArtifact,
EtherToken: (EtherToken as any) as ContractArtifact,
TokenRegistry: (TokenRegistry as any) as ContractArtifact,
MaliciousToken: (MaliciousToken as any) as ContractArtifact,
TokenTransferProxy: (TokenTransferProxy as any) as ContractArtifact,
MultiSigWalletWithTimeLock: (MultiSigWalletWithTimeLock as any) as ContractArtifact,
MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress: (MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress as any) as ContractArtifact,
};

View File

@ -100,19 +100,3 @@ export enum ContractName {
EtherDelta = 'EtherDelta',
Arbitrage = 'Arbitrage',
}
export interface Artifact {
contract_name: ContractName;
networks: {
[networkId: number]: {
abi: ContractAbi;
solc_version: string;
keccak256: string;
optimizer_enabled: number;
unlinked_binary: string;
updated_at: number;
address: string;
constructor_args: string;
};
};
}

View File

@ -13,13 +13,13 @@ import { constants } from './utils/constants';
import { consoleReporter } from './utils/error_reporter';
import { CliOptions, CompilerOptions, DeployerOptions } from './utils/types';
const DEFAULT_OPTIMIZER_ENABLED = false;
const DEFAULT_CONTRACTS_DIR = path.resolve('src/contracts');
const DEFAULT_ARTIFACTS_DIR = path.resolve('src/artifacts');
const DEFAULT_NETWORK_ID = 50;
const DEFAULT_JSONRPC_URL = 'http://localhost:8545';
const DEFAULT_GAS_PRICE = (10 ** 9 * 2).toString();
const DEFAULT_CONTRACTS_LIST = '*';
const SEPARATOR = ',';
/**
* Compiles all contracts with options passed in through CLI.
@ -28,10 +28,8 @@ const DEFAULT_CONTRACTS_LIST = '*';
async function onCompileCommandAsync(argv: CliOptions): Promise<void> {
const opts: CompilerOptions = {
contractsDir: argv.contractsDir,
networkId: argv.networkId,
optimizerEnabled: argv.shouldOptimize,
artifactsDir: argv.artifactsDir,
specifiedContracts: getContractsSetFromList(argv.contracts),
contracts: argv.contracts === DEFAULT_CONTRACTS_LIST ? DEFAULT_CONTRACTS_LIST : argv.contracts.split(SEPARATOR),
};
await commands.compileAsync(opts);
}
@ -46,10 +44,8 @@ async function onDeployCommandAsync(argv: CliOptions): Promise<void> {
const networkId = await web3Wrapper.getNetworkIdAsync();
const compilerOpts: CompilerOptions = {
contractsDir: argv.contractsDir,
networkId,
optimizerEnabled: argv.shouldOptimize,
artifactsDir: argv.artifactsDir,
specifiedContracts: getContractsSetFromList(argv.contracts),
contracts: argv.contracts === DEFAULT_CONTRACTS_LIST ? DEFAULT_CONTRACTS_LIST : argv.contracts.split(SEPARATOR),
};
await commands.compileAsync(compilerOpts);
@ -58,45 +54,63 @@ async function onDeployCommandAsync(argv: CliOptions): Promise<void> {
from: argv.account,
};
const deployerOpts: DeployerOptions = {
artifactsDir: argv.artifactsDir,
artifactsDir: argv.artifactsDir || DEFAULT_ARTIFACTS_DIR,
jsonrpcUrl: argv.jsonrpcUrl,
networkId,
defaults,
};
const deployerArgsString = argv.args as string;
const deployerArgs = deployerArgsString.split(',');
const deployerArgsString = argv.constructorArgs as string;
const deployerArgs = deployerArgsString.split(SEPARATOR);
await commands.deployAsync(argv.contract as string, deployerArgs, deployerOpts);
}
/**
* Creates a set of contracts to compile.
* @param contracts Comma separated list of contracts to compile
*/
function getContractsSetFromList(contracts: string): Set<string> {
const specifiedContracts = new Set();
if (contracts === '*') {
return new Set(['*']);
}
const contractsArray = contracts.split(',');
_.forEach(contractsArray, contractName => {
specifiedContracts.add(contractName);
});
return specifiedContracts;
}
/**
* Provides extra required options for deploy command.
* Adds additional required options for when the user is calling the deploy command.
* @param yargsInstance yargs instance provided in builder function callback.
*/
function deployCommandBuilder(yargsInstance: any) {
return yargsInstance
.option('network-id', {
type: 'number',
default: DEFAULT_NETWORK_ID,
description: 'mainnet=1, kovan=42, testrpc=50',
})
.option('contract', {
type: 'string',
description: 'name of contract to deploy, exluding .sol extension',
description: 'name of contract to deploy, excluding .sol extension',
})
.option('args', {
.option('constructor-args', {
type: 'string',
description: 'comma separated list of constructor args to deploy contract with',
})
.demandOption(['contract', 'args'])
.option('jsonrpc-url', {
type: 'string',
default: DEFAULT_JSONRPC_URL,
description: 'url of JSON RPC',
})
.option('account', {
type: 'string',
description: 'account to use for deploying contracts',
})
.option('gas-price', {
type: 'string',
default: DEFAULT_GAS_PRICE,
description: 'gasPrice to be used for transactions',
})
.demandOption(['contract', 'args', 'account'])
.help().argv;
}
/**
* Adds additional required options for when the user is calling the compile command.
* @param yargsInstance yargs instance provided in builder function callback.
*/
function compileCommandBuilder(yargsInstance: any) {
return yargsInstance
.option('contracts', {
type: 'string',
default: DEFAULT_CONTRACTS_LIST,
description: 'comma separated list of contracts to compile',
})
.help().argv;
}
@ -105,44 +119,14 @@ function deployCommandBuilder(yargsInstance: any) {
return yargs
.option('contracts-dir', {
type: 'string',
default: DEFAULT_CONTRACTS_DIR,
description: 'path of contracts directory to compile',
})
.option('network-id', {
type: 'number',
default: DEFAULT_NETWORK_ID,
description: 'mainnet=1, kovan=42, testrpc=50',
})
.option('should-optimize', {
type: 'boolean',
default: DEFAULT_OPTIMIZER_ENABLED,
description: 'enable optimizer',
})
.option('artifacts-dir', {
type: 'string',
default: DEFAULT_ARTIFACTS_DIR,
description: 'path to write contracts artifacts to',
})
.option('jsonrpc-url', {
type: 'string',
default: DEFAULT_JSONRPC_URL,
description: 'url of JSON RPC',
})
.option('gas-price', {
type: 'string',
default: DEFAULT_GAS_PRICE,
description: 'gasPrice to be used for transactions',
})
.option('account', {
type: 'string',
description: 'account to use for deploying contracts',
})
.option('contracts', {
type: 'string',
default: DEFAULT_CONTRACTS_LIST,
description: 'comma separated list of contracts to compile',
})
.command('compile', 'compile contracts', identityCommandBuilder, consoleReporter(onCompileCommandAsync))
.demandCommand(1)
.command('compile', 'compile contracts', compileCommandBuilder, consoleReporter(onCompileCommandAsync))
.command(
'deploy',
'deploy a single contract with provided arguments',

View File

@ -6,6 +6,7 @@ import {
FSResolver,
NameResolver,
NPMResolver,
RelativeFSResolver,
Resolver,
URLResolver,
} from '@0xproject/sol-resolver';
@ -38,11 +39,30 @@ import {
ContractNetworks,
ContractSourceData,
ContractSpecificSourceData,
ContractVersionData,
} from './utils/types';
import { utils } from './utils/utils';
type TYPE_ALL_FILES_IDENTIFIER = '*';
const ALL_CONTRACTS_IDENTIFIER = '*';
const ALL_FILES_IDENTIFIER = '*';
const SOLC_BIN_DIR = path.join(__dirname, '..', '..', 'solc_bin');
const DEFAULT_CONTRACTS_DIR = path.resolve('contracts');
const DEFAULT_ARTIFACTS_DIR = path.resolve('artifacts');
// Solc compiler settings cannot be configured from the commandline.
// If you need this configured, please create a `compiler.json` config file
// with your desired configurations.
const DEFAULT_COMPILER_SETTINGS: solc.CompilerSettings = {
optimizer: {
enabled: false,
},
outputSelection: {
[ALL_FILES_IDENTIFIER]: {
[ALL_CONTRACTS_IDENTIFIER]: ['abi', 'evm.bytecode.object'],
},
},
};
const CONFIG_FILE = 'compiler.json';
/**
* The Compiler facilitates compiling Solidity smart contracts and saves the results
@ -52,26 +72,28 @@ export class Compiler {
private _resolver: Resolver;
private _nameResolver: NameResolver;
private _contractsDir: string;
private _networkId: number;
private _optimizerEnabled: boolean;
private _compilerSettings: solc.CompilerSettings;
private _artifactsDir: string;
private _specifiedContracts: Set<string> = new Set();
private _specifiedContracts: string[] | TYPE_ALL_FILES_IDENTIFIER;
/**
* Instantiates a new instance of the Compiler class.
* @param opts Options specifying directories, network, and optimization settings.
* @return An instance of the Compiler class.
*/
constructor(opts: CompilerOptions) {
this._contractsDir = opts.contractsDir;
this._networkId = opts.networkId;
this._optimizerEnabled = opts.optimizerEnabled;
this._artifactsDir = opts.artifactsDir;
this._specifiedContracts = opts.specifiedContracts;
// TODO: Look for config file in parent directories if not found in current directory
const config: CompilerOptions = fs.existsSync(CONFIG_FILE)
? JSON.parse(fs.readFileSync(CONFIG_FILE).toString())
: {};
this._contractsDir = opts.contractsDir || config.contractsDir || DEFAULT_CONTRACTS_DIR;
this._compilerSettings = opts.compilerSettings || config.compilerSettings || DEFAULT_COMPILER_SETTINGS;
this._artifactsDir = opts.artifactsDir || config.artifactsDir || DEFAULT_ARTIFACTS_DIR;
this._specifiedContracts = opts.contracts || config.contracts || ALL_CONTRACTS_IDENTIFIER;
this._nameResolver = new NameResolver(path.resolve(this._contractsDir));
const resolver = new FallthroughResolver();
resolver.appendResolver(new URLResolver());
const packagePath = path.resolve('');
resolver.appendResolver(new NPMResolver(packagePath));
resolver.appendResolver(new RelativeFSResolver(this._contractsDir));
resolver.appendResolver(new FSResolver());
resolver.appendResolver(this._nameResolver);
this._resolver = resolver;
@ -83,13 +105,13 @@ export class Compiler {
await createDirIfDoesNotExistAsync(this._artifactsDir);
await createDirIfDoesNotExistAsync(SOLC_BIN_DIR);
let contractNamesToCompile: string[] = [];
if (this._specifiedContracts.has(ALL_CONTRACTS_IDENTIFIER)) {
if (this._specifiedContracts === ALL_CONTRACTS_IDENTIFIER) {
const allContracts = this._nameResolver.getAll();
contractNamesToCompile = _.map(allContracts, contractSource =>
path.basename(contractSource.path, constants.SOLIDITY_FILE_EXTENSION),
);
} else {
contractNamesToCompile = Array.from(this._specifiedContracts.values());
contractNamesToCompile = this._specifiedContracts;
}
for (const contractNameToCompile of contractNamesToCompile) {
await this._compileContractAsync(contractNameToCompile);
@ -101,17 +123,18 @@ export class Compiler {
*/
private async _compileContractAsync(contractName: string): Promise<void> {
const contractSource = this._resolver.resolve(contractName);
const absoluteContractPath = path.join(this._contractsDir, contractSource.path);
const currentArtifactIfExists = await getContractArtifactIfExistsAsync(this._artifactsDir, contractName);
const sourceTreeHashHex = `0x${this._getSourceTreeHash(contractSource.path).toString('hex')}`;
const sourceTreeHashHex = `0x${this._getSourceTreeHash(absoluteContractPath).toString('hex')}`;
let shouldCompile = false;
if (_.isUndefined(currentArtifactIfExists)) {
shouldCompile = true;
} else {
const currentArtifact = currentArtifactIfExists as ContractArtifact;
shouldCompile =
currentArtifact.networks[this._networkId].optimizer_enabled !== this._optimizerEnabled ||
currentArtifact.networks[this._networkId].source_tree_hash !== sourceTreeHashHex;
const isUserOnLatestVersion = currentArtifact.schemaVersion === constants.LATEST_ARTIFACT_VERSION;
const didCompilerSettingsChange = !_.isEqual(currentArtifact.compiler.settings, this._compilerSettings);
const didSourceChange = currentArtifact.sourceTreeHashHex !== sourceTreeHashHex;
shouldCompile = isUserOnLatestVersion || didCompilerSettingsChange || didSourceChange;
}
if (!shouldCompile) {
return;
@ -139,30 +162,14 @@ export class Compiler {
logUtils.log(`Compiling ${contractName} with Solidity v${solcVersion}...`);
const source = contractSource.source;
const absoluteFilePath = contractSource.path;
const standardInput: solc.StandardInput = {
language: 'Solidity',
sources: {
[absoluteFilePath]: {
urls: [`file://${absoluteFilePath}`],
},
},
settings: {
optimizer: {
enabled: this._optimizerEnabled,
},
outputSelection: {
'*': {
'*': [
'abi',
'evm.bytecode.object',
'evm.bytecode.sourceMap',
'evm.deployedBytecode.object',
'evm.deployedBytecode.sourceMap',
],
},
[contractSource.path]: {
content: contractSource.source,
},
},
settings: this._compilerSettings,
};
const compiled: solc.StandardOutput = JSON.parse(
solcInstance.compileStandardWrapper(JSON.stringify(standardInput), importPath => {
@ -188,34 +195,28 @@ export class Compiler {
});
}
}
const compiledData = compiled.contracts[absoluteFilePath][contractName];
const compiledData = compiled.contracts[contractSource.path][contractName];
if (_.isUndefined(compiledData)) {
throw new Error(
`Contract ${contractName} not found in ${absoluteFilePath}. Please make sure your contract has the same name as it's file name`,
`Contract ${contractName} not found in ${
contractSource.path
}. Please make sure your contract has the same name as it's file name`,
);
}
const abi: ContractAbi = compiledData.abi;
const bytecode = `0x${compiledData.evm.bytecode.object}`;
const runtimeBytecode = `0x${compiledData.evm.deployedBytecode.object}`;
const sourceMap = compiledData.evm.bytecode.sourceMap;
const sourceMapRuntime = compiledData.evm.deployedBytecode.sourceMap;
const unresolvedSourcePaths = _.keys(compiled.sources);
const sources = _.map(
unresolvedSourcePaths,
unresolvedSourcePath => this._resolver.resolve(unresolvedSourcePath).path,
const sourceCodes = _.mapValues(
compiled.sources,
(_1, sourceFilePath) => this._resolver.resolve(sourceFilePath).source,
);
const updated_at = Date.now();
const contractNetworkData: ContractNetworkData = {
solc_version: solcVersion,
source_tree_hash: sourceTreeHashHex,
optimizer_enabled: this._optimizerEnabled,
abi,
bytecode,
runtime_bytecode: runtimeBytecode,
updated_at,
source_map: sourceMap,
source_map_runtime: sourceMapRuntime,
sources,
const contractVersion: ContractVersionData = {
compilerOutput: compiledData,
sources: compiled.sources,
sourceCodes,
sourceTreeHashHex,
compiler: {
name: 'solc',
version: solcVersion,
settings: this._compilerSettings,
},
};
let newArtifact: ContractArtifact;
@ -223,17 +224,14 @@ export class Compiler {
const currentArtifact = currentArtifactIfExists as ContractArtifact;
newArtifact = {
...currentArtifact,
networks: {
...currentArtifact.networks,
[this._networkId]: contractNetworkData,
},
...contractVersion,
};
} else {
newArtifact = {
contract_name: contractName,
networks: {
[this._networkId]: contractNetworkData,
},
schemaVersion: constants.LATEST_ARTIFACT_VERSION,
contractName,
...contractVersion,
networks: {},
};
}

View File

@ -2,6 +2,7 @@ import { AbiType, ConstructorAbi, ContractAbi, Provider, TxData } from '@0xproje
import { logUtils } from '@0xproject/utils';
import { Web3Wrapper } from '@0xproject/web3-wrapper';
import * as _ from 'lodash';
import * as solc from 'solc';
import * as Web3 from 'web3';
import { Contract } from './utils/contract';
@ -28,7 +29,20 @@ export class Deployer {
private _artifactsDir: string;
private _networkId: number;
private _defaults: Partial<TxData>;
/**
* Gets data for current version stored in artifact.
* @param contractArtifact The contract artifact.
* @return Version specific contract data.
*/
private static _getContractCompilerOutputFromArtifactIfExists(
contractArtifact: ContractArtifact,
): solc.StandardContractOutput {
const compilerOutputIfExists = contractArtifact.compilerOutput;
if (_.isUndefined(compilerOutputIfExists)) {
throw new Error(`Compiler output not found in artifact for contract: ${contractArtifact.contractName}`);
}
return compilerOutputIfExists;
}
/**
* Instantiate a new instance of the Deployer class.
* @param opts Deployer options, including either an RPC url or Provider instance.
@ -58,10 +72,8 @@ export class Deployer {
*/
public async deployAsync(contractName: string, args: any[] = []): Promise<Web3.ContractInstance> {
const contractArtifactIfExists: ContractArtifact = this._loadContractArtifactIfExists(contractName);
const contractNetworkDataIfExists: ContractNetworkData = this._getContractNetworkDataFromArtifactIfExists(
contractArtifactIfExists,
);
const data = contractNetworkDataIfExists.bytecode;
const compilerOutput = Deployer._getContractCompilerOutputFromArtifactIfExists(contractArtifactIfExists);
const data = compilerOutput.evm.bytecode.object;
const from = await this._getFromAddressAsync();
const gas = await this._getAllowableGasEstimateAsync(data);
const txData = {
@ -70,7 +82,10 @@ export class Deployer {
data,
gas,
};
const abi = contractNetworkDataIfExists.abi;
if (_.isUndefined(compilerOutput.abi)) {
throw new Error(`ABI not found in ${contractName} artifacts`);
}
const abi = compilerOutput.abi;
const constructorAbi = _.find(abi, { type: AbiType.Constructor }) as ConstructorAbi;
const constructorArgs = _.isUndefined(constructorAbi) ? [] : constructorAbi.inputs;
if (constructorArgs.length !== args.length) {
@ -138,15 +153,16 @@ export class Deployer {
args: any[],
): Promise<void> {
const contractArtifactIfExists: ContractArtifact = this._loadContractArtifactIfExists(contractName);
const contractNetworkDataIfExists: ContractNetworkData = this._getContractNetworkDataFromArtifactIfExists(
contractArtifactIfExists,
);
const abi = contractNetworkDataIfExists.abi;
const compilerOutput = Deployer._getContractCompilerOutputFromArtifactIfExists(contractArtifactIfExists);
if (_.isUndefined(compilerOutput.abi)) {
throw new Error(`ABI not found in ${contractName} artifacts`);
}
const abi = compilerOutput.abi;
const encodedConstructorArgs = encoder.encodeConstructorArgsFromAbi(args, abi);
const newContractData = {
...contractNetworkDataIfExists,
const newContractData: ContractNetworkData = {
address: contractAddress,
constructor_args: encodedConstructorArgs,
links: {},
constructorArgs: encodedConstructorArgs,
};
const newArtifact = {
...contractArtifactIfExists,
@ -173,18 +189,6 @@ export class Deployer {
throw new Error(`Artifact not found for contract: ${contractName} at ${artifactPath}`);
}
}
/**
* Gets data for current networkId stored in artifact.
* @param contractArtifact The contract artifact.
* @return Network specific contract data.
*/
private _getContractNetworkDataFromArtifactIfExists(contractArtifact: ContractArtifact): ContractNetworkData {
const contractNetworkDataIfExists = contractArtifact.networks[this._networkId];
if (_.isUndefined(contractNetworkDataIfExists)) {
throw new Error(`Data not found in artifact for contract: ${contractArtifact.contract_name}`);
}
return contractNetworkDataIfExists;
}
/**
* Gets the address to use for sending a transaction.
* @return The default from address. If not specified, returns the first address accessible by web3.

View File

@ -1,2 +1,3 @@
export { Deployer } from './deployer';
export { Compiler } from './compiler';
export { ContractArtifact, ContractNetworks } from './utils/types';

View File

@ -1,4 +1,5 @@
export const constants = {
SOLIDITY_FILE_EXTENSION: '.sol',
BASE_COMPILER_URL: 'https://ethereum.github.io/solc-bin/bin/',
LATEST_ARTIFACT_VERSION: '2.0.0',
};

View File

@ -1,4 +1,5 @@
import { ContractAbi, Provider, TxData } from '@0xproject/types';
import * as solc from 'solc';
import * as Web3 from 'web3';
import * as yargs from 'yargs';
@ -9,28 +10,40 @@ export enum AbiType {
Fallback = 'fallback',
}
export interface ContractArtifact {
contract_name: string;
export interface ContractArtifact extends ContractVersionData {
schemaVersion: string;
contractName: string;
networks: ContractNetworks;
}
export interface ContractVersionData {
compiler: {
name: 'solc';
version: string;
settings: solc.CompilerSettings;
};
sources: {
[sourceName: string]: {
id: number;
};
};
sourceCodes: {
[sourceName: string]: string;
};
sourceTreeHashHex: string;
compilerOutput: solc.StandardContractOutput;
}
export interface ContractNetworks {
[key: number]: ContractNetworkData;
[networkId: number]: ContractNetworkData;
}
export interface ContractNetworkData {
solc_version: string;
optimizer_enabled: boolean;
source_tree_hash: string;
abi: ContractAbi;
bytecode: string;
runtime_bytecode: string;
address?: string;
constructor_args?: string;
updated_at: number;
source_map: string;
source_map_runtime: string;
sources: string[];
address: string;
links: {
[linkName: string]: string;
};
constructorArgs: string;
}
export interface SolcErrors {
@ -42,7 +55,6 @@ export interface CliOptions extends yargs.Arguments {
contractsDir: string;
jsonrpcUrl: string;
networkId: number;
shouldOptimize: boolean;
gasPrice: string;
account?: string;
contract?: string;
@ -50,11 +62,10 @@ export interface CliOptions extends yargs.Arguments {
}
export interface CompilerOptions {
contractsDir: string;
networkId: number;
optimizerEnabled: boolean;
artifactsDir: string;
specifiedContracts: Set<string>;
contractsDir?: string;
artifactsDir?: string;
compilerSettings?: solc.CompilerSettings;
contracts?: string[] | '*';
}
export interface BaseDeployerOptions {

View File

@ -19,9 +19,7 @@ describe('#Compiler', function() {
const compilerOpts: CompilerOptions = {
artifactsDir,
contractsDir,
networkId: constants.networkId,
optimizerEnabled: constants.optimizerEnabled,
specifiedContracts: new Set(constants.specifiedContracts),
contracts: constants.contracts,
};
const compiler = new Compiler(compilerOpts);
beforeEach((done: DoneCallback) => {
@ -39,9 +37,8 @@ describe('#Compiler', function() {
};
const exchangeArtifactString = await fsWrapper.readFileAsync(exchangeArtifactPath, opts);
const exchangeArtifact: ContractArtifact = JSON.parse(exchangeArtifactString);
const exchangeContractData: ContractNetworkData = exchangeArtifact.networks[constants.networkId];
// The last 43 bytes of the binaries are metadata which may not be equivalent
const unlinkedBinaryWithoutMetadata = exchangeContractData.bytecode.slice(0, -86);
const unlinkedBinaryWithoutMetadata = exchangeArtifact.compilerOutput.evm.bytecode.object.slice(0, -86);
const exchangeBinaryWithoutMetadata = exchange_binary.slice(0, -86);
expect(unlinkedBinaryWithoutMetadata).to.equal(exchangeBinaryWithoutMetadata);
});

View File

@ -20,9 +20,7 @@ describe('#Deployer', () => {
const compilerOpts: CompilerOptions = {
artifactsDir,
contractsDir,
networkId: constants.networkId,
optimizerEnabled: constants.optimizerEnabled,
specifiedContracts: new Set(constants.specifiedContracts),
contracts: constants.contracts,
};
const compiler = new Compiler(compilerOpts);
const deployerOpts = {
@ -56,8 +54,7 @@ describe('#Deployer', () => {
const exchangeContractData: ContractNetworkData = exchangeArtifact.networks[constants.networkId];
const exchangeAddress = exchangeContractInstance.address;
expect(exchangeAddress).to.not.equal(undefined);
expect(exchangeContractData.address).to.equal(undefined);
expect(exchangeContractData.constructor_args).to.equal(undefined);
expect(exchangeContractData).to.equal(undefined);
});
});
describe('#deployAndSaveAsync', () => {
@ -72,7 +69,7 @@ describe('#Deployer', () => {
const exchangeContractData: ContractNetworkData = exchangeArtifact.networks[constants.networkId];
const exchangeAddress = exchangeContractInstance.address;
expect(exchangeAddress).to.be.equal(exchangeContractData.address);
expect(constructor_args).to.be.equal(exchangeContractData.constructor_args);
expect(constructor_args).to.be.equal(exchangeContractData.constructorArgs);
});
});
});

File diff suppressed because one or more lines are too long

View File

@ -7,5 +7,5 @@ export const constants = {
timeoutMs: 30000,
zrxTokenAddress: '0xe41d2489571d322189246dafa5ebde1f4699f498',
tokenTransferProxyAddress: '0x8da0d80f5007ef1e431dd2127178d224e32c2ef4',
specifiedContracts: '*',
contracts: '*' as '*',
};

View File

@ -13,7 +13,7 @@ export const coverage = {
return coverageSubprovider;
},
_getCoverageSubprovider(): CoverageSubprovider {
const artifactsPath = '../migrations/src/artifacts';
const artifactsPath = '../migrations/artifacts/1.0.0';
const contractsPath = 'src/contracts';
const networkId = 50;
const defaultFromAddress = constants.TESTRPC_FIRST_ADDRESS;

View File

@ -0,0 +1,15 @@
{
"compilerSettings": {
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode.object",
"evm.bytecode.sourceMap",
"evm.deployedBytecode.object",
"evm.deployedBytecode.sourceMap"
]
}
}
}
}

View File

@ -12,13 +12,13 @@
"build": "tsc",
"test": "run-s build run_mocha",
"test:coverage": "SOLIDITY_COVERAGE=true run-s build run_mocha coverage:report:text coverage:report:lcov",
"run_mocha": "mocha lib/test/**/*.js --bail --exit",
"run_mocha": "mocha lib/test/**/*_test.js lib/test/global_hooks.js --bail --exit",
"generate_contract_wrappers": "node ../abi-gen/lib/index.js --abis 'artifacts/Metacoin.json' --template ../contract_templates/contract.handlebars --partials '../contract_templates/partials/**/*.handlebars' --output src/contract_wrappers --backend ethers && prettier --write 'src/contract_wrappers/**.ts'",
"coverage:report:text": "istanbul report text",
"coverage:report:html": "istanbul report html && open coverage/index.html",
"coverage:report:lcov": "istanbul report lcov",
"test:circleci": "yarn test:coverage",
"compile": "node ../deployer/lib/src/cli.js compile --contracts Metacoin --contracts-dir contracts --artifacts-dir artifacts"
"compile": "node ../deployer/lib/src/cli.js compile"
},
"author": "",
"license": "Apache-2.0",

View File

View File

@ -0,0 +1,18 @@
{
"contracts": ["SimpleStorage"],
"contractsDir": "test/fixtures/contracts",
"artifactsDir": "test/fixtures/artifacts",
"compilerSettings": {
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode.object",
"evm.bytecode.sourceMap",
"evm.deployedBytecode.object",
"evm.deployedBytecode.sourceMap"
]
}
}
}
}

View File

@ -14,7 +14,7 @@
"run_mocha": "mocha lib/test/**/*_test.js --exit",
"clean": "shx rm -rf lib scripts",
"build": "copyfiles 'test/fixtures/**/*' ./lib && tsc && copyfiles -u 3 './lib/src/monorepo_scripts/**/*' ./scripts",
"compile_test": "node ../deployer/lib/src/cli.js compile --contracts SimpleStorage --contracts-dir test/fixtures/contracts --artifacts-dir test/fixtures/artifacts",
"compile_test": "node ../deployer/lib/src/cli.js compile",
"manual:postpublish": "yarn build; node ./scripts/postpublish.js",
"docs:stage": "yarn build && node ./scripts/stage_docs.js",
"docs:json": "typedoc --excludePrivate --excludeExternals --target ES5 --json $JSON_FILE_PATH $PROJECT_FILES",

View File

@ -8,25 +8,24 @@ import { ContractData } from './types';
export const collectContractsData = (artifactsPath: string, sourcesPath: string, networkId: number) => {
const artifactsGlob = `${artifactsPath}/**/*.json`;
const artifactFileNames = glob.sync(artifactsGlob, { absolute: true });
const contractsDataIfExists: Array<ContractData | {}> = _.map(artifactFileNames, artifactFileName => {
const contractsData: ContractData[] = [];
_.forEach(artifactFileNames, artifactFileName => {
const artifact = JSON.parse(fs.readFileSync(artifactFileName).toString());
const sources = artifact.networks[networkId].sources;
const contractName = artifact.contract_name;
const sources = _.keys(artifact.sources);
const contractName = artifact.contractName;
// We don't compute coverage for dependencies
const sourceCodes = _.map(sources, (source: string) => fs.readFileSync(source).toString());
if (_.isUndefined(artifact.networks[networkId])) {
throw new Error(`No ${contractName} artifacts found for networkId ${networkId}`);
}
const sourceCodes = _.map(sources, (source: string) =>
fs.readFileSync(path.join(sourcesPath, source)).toString(),
);
const contractData = {
sourceCodes,
sources,
sourceMap: artifact.networks[networkId].source_map,
sourceMapRuntime: artifact.networks[networkId].source_map_runtime,
runtimeBytecode: artifact.networks[networkId].runtime_bytecode,
bytecode: artifact.networks[networkId].bytecode,
bytecode: artifact.compilerOutput.evm.bytecode.object,
sourceMap: artifact.compilerOutput.evm.bytecode.sourceMap,
runtimeBytecode: artifact.compilerOutput.evm.deployedBytecode.object,
sourceMapRuntime: artifact.compilerOutput.evm.deployedBytecode.sourceMap,
};
return contractData;
contractsData.push(contractData);
});
const contractsData = _.filter(contractsDataIfExists, contractData => !_.isEmpty(contractData)) as ContractData[];
return contractsData;
};

View File

@ -29,10 +29,31 @@ import {
import { utils } from './utils';
export class CoverageManager {
private _sourcesPath: string;
private _traceInfos: TraceInfo[] = [];
private _contractsData: ContractData[] = [];
private _getContractCodeAsync: (address: string) => Promise<string>;
private static _getSingleFileCoverageForTrace(
constructor(
artifactsPath: string,
sourcesPath: string,
networkId: number,
getContractCodeAsync: (address: string) => Promise<string>,
) {
this._getContractCodeAsync = getContractCodeAsync;
this._sourcesPath = sourcesPath;
this._contractsData = collectContractsData(artifactsPath, this._sourcesPath, networkId);
}
public appendTraceInfo(traceInfo: TraceInfo): void {
this._traceInfos.push(traceInfo);
}
public async writeCoverageAsync(): Promise<void> {
const finalCoverage = await this._computeCoverageAsync();
const jsonReplacer: null = null;
const numberOfJsonSpaces = 4;
const stringifiedCoverage = JSON.stringify(finalCoverage, jsonReplacer, numberOfJsonSpaces);
fs.writeFileSync('coverage/coverage.json', stringifiedCoverage);
}
private _getSingleFileCoverageForTrace(
contractData: ContractData,
coveredPcs: number[],
pcToSourceRange: { [programCounter: number]: SourceRange },
@ -94,11 +115,12 @@ export class CoverageManager {
);
statementCoverage[modifierStatementId] = isModifierCovered;
}
const absoluteFileName = path.join(this._sourcesPath, fileName);
const partialCoverage = {
[contractData.sources[fileIndex]]: {
[absoluteFileName]: {
...coverageEntriesDescription,
l: {}, // It's able to derive it from statement coverage
path: fileName,
path: absoluteFileName,
f: functionCoverage,
s: statementCoverage,
b: branchCoverage,
@ -106,31 +128,13 @@ export class CoverageManager {
};
return partialCoverage;
}
constructor(
artifactsPath: string,
sourcesPath: string,
networkId: number,
getContractCodeAsync: (address: string) => Promise<string>,
) {
this._getContractCodeAsync = getContractCodeAsync;
this._contractsData = collectContractsData(artifactsPath, sourcesPath, networkId);
}
public appendTraceInfo(traceInfo: TraceInfo): void {
this._traceInfos.push(traceInfo);
}
public async writeCoverageAsync(): Promise<void> {
const finalCoverage = await this._computeCoverageAsync();
const jsonReplacer: null = null;
const numberOfJsonSpaces = 4;
const stringifiedCoverage = JSON.stringify(finalCoverage, jsonReplacer, numberOfJsonSpaces);
fs.writeFileSync('coverage/coverage.json', stringifiedCoverage);
}
private async _computeCoverageAsync(): Promise<Coverage> {
const collector = new Collector();
for (const traceInfo of this._traceInfos) {
if (traceInfo.address !== constants.NEW_CONTRACT) {
// Runtime transaction
const runtimeBytecode = (traceInfo as TraceInfoExistingContract).runtimeBytecode;
let runtimeBytecode = (traceInfo as TraceInfoExistingContract).runtimeBytecode;
runtimeBytecode = utils.removeHexPrefix(runtimeBytecode);
const contractData = _.find(this._contractsData, { runtimeBytecode }) as ContractData;
if (_.isUndefined(contractData)) {
throw new Error(`Transaction to an unknown address: ${traceInfo.address}`);
@ -144,7 +148,7 @@ export class CoverageManager {
contractData.sources,
);
for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) {
const singleFileCoverageForTrace = CoverageManager._getSingleFileCoverageForTrace(
const singleFileCoverageForTrace = this._getSingleFileCoverageForTrace(
contractData,
traceInfo.coveredPcs,
pcToSourceRange,
@ -154,7 +158,8 @@ export class CoverageManager {
}
} else {
// Contract creation transaction
const bytecode = (traceInfo as TraceInfoNewContract).bytecode;
let bytecode = (traceInfo as TraceInfoNewContract).bytecode;
bytecode = utils.removeHexPrefix(bytecode);
const contractData = _.find(this._contractsData, contractDataCandidate =>
bytecode.startsWith(contractDataCandidate.bytecode),
) as ContractData;
@ -170,7 +175,7 @@ export class CoverageManager {
contractData.sources,
);
for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) {
const singleFileCoverageForTrace = CoverageManager._getSingleFileCoverageForTrace(
const singleFileCoverageForTrace = this._getSingleFileCoverageForTrace(
contractData,
traceInfo.coveredPcs,
pcToSourceRange,

View File

@ -4,6 +4,10 @@ export const utils = {
compareLineColumn(lhs: LineColumn, rhs: LineColumn): number {
return lhs.line !== rhs.line ? lhs.line - rhs.line : lhs.column - rhs.column;
},
removeHexPrefix(hex: string): string {
const hexPrefix = '0x';
return hex.startsWith(hexPrefix) ? hex.slice(hexPrefix.length) : hex;
},
isRangeInside(childRange: SingleFileSourceRange, parentRange: SingleFileSourceRange): boolean {
return (
utils.compareLineColumn(parentRange.start, childRange.start) <= 0 &&

View File

@ -3,6 +3,7 @@ export { FallthroughResolver } from './resolvers/fallthrough_resolver';
export { URLResolver } from './resolvers/url_resolver';
export { NPMResolver } from './resolvers/npm_resolver';
export { FSResolver } from './resolvers/fs_resolver';
export { RelativeFSResolver } from './resolvers/relative_fs_resolver';
export { NameResolver } from './resolvers/name_resolver';
export { EnumerableResolver } from './resolvers/enumerable_resolver';
export { Resolver } from './resolvers/resolver';

View File

@ -18,7 +18,8 @@ export class NameResolver extends EnumerableResolver {
const onFile = (filePath: string) => {
const contractName = path.basename(filePath, SOLIDITY_FILE_EXTENSION);
if (contractName === lookupContractName) {
const source = fs.readFileSync(filePath).toString();
const absoluteContractPath = path.join(this._contractsDir, filePath);
const source = fs.readFileSync(absoluteContractPath).toString();
contractSource = {
source,
path: filePath,
@ -35,7 +36,8 @@ export class NameResolver extends EnumerableResolver {
const contractSources: ContractSource[] = [];
const onFile = (filePath: string) => {
const contractName = path.basename(filePath, SOLIDITY_FILE_EXTENSION);
const source = fs.readFileSync(filePath).toString();
const absoluteContractPath = path.join(this._contractsDir, filePath);
const source = fs.readFileSync(absoluteContractPath).toString();
const contractSource = {
source,
path: filePath,
@ -54,9 +56,10 @@ export class NameResolver extends EnumerableResolver {
throw new Error(`No directory found at ${dirPath}`);
}
for (const fileName of dirContents) {
const entryPath = path.join(dirPath, fileName);
const isDirectory = fs.lstatSync(entryPath).isDirectory();
const isComplete = isDirectory ? this._traverseContractsDir(entryPath, onFile) : onFile(entryPath);
const absoluteEntryPath = path.join(dirPath, fileName);
const isDirectory = fs.lstatSync(absoluteEntryPath).isDirectory();
const entryPath = path.relative(this._contractsDir, absoluteEntryPath);
const isComplete = isDirectory ? this._traverseContractsDir(absoluteEntryPath, onFile) : onFile(entryPath);
if (isComplete) {
return isComplete;
}

View File

@ -0,0 +1,26 @@
import * as fs from 'fs';
import * as path from 'path';
import { ContractSource } from '../types';
import { Resolver } from './resolver';
export class RelativeFSResolver extends Resolver {
private _contractsDir: string;
constructor(contractsDir: string) {
super();
this._contractsDir = contractsDir;
}
// tslint:disable-next-line:prefer-function-over-method
public resolveIfExists(importPath: string): ContractSource | undefined {
const filePath = path.join(this._contractsDir, importPath);
if (fs.existsSync(filePath)) {
const fileContent = fs.readFileSync(filePath).toString();
return {
source: fileContent,
path: importPath,
};
}
return undefined;
}
}

View File

@ -59,32 +59,33 @@ declare module 'solc' {
| 'evm.gasEstimates'
| 'ewasm.wast'
| 'ewasm.wasm';
export interface CompilerSettings {
remappings?: string[];
optimizer?: {
enabled: boolean;
runs?: number;
};
evmVersion?: 'homestead' | 'tangerineWhistle' | 'spuriousDragon' | 'byzantium' | 'constantinople';
metadata?: {
useLiteralContent: true;
};
libraries?: {
[fileName: string]: {
[libName: string]: string;
};
};
outputSelection: {
[fileName: string]: {
[contractName: string]: OutputField[];
};
};
}
export interface StandardInput {
language: 'Solidity' | 'serpent' | 'lll' | 'assembly';
sources: {
[fileName: string]: Source;
};
settings: {
remappings?: string[];
optimizer?: {
enabled: boolean;
runs?: number;
};
evmVersion?: 'homestead' | 'tangerineWhistle' | 'spuriousDragon' | 'byzantium' | 'constantinople';
metadata?: {
useLiteralContent: true;
};
libraries?: {
[fileName: string]: {
[libName: string]: string;
};
};
outputSelection: {
[fileName: string]: {
[contractName: string]: OutputField[];
};
};
};
settings: CompilerSettings;
}
export type ErrorType =
| 'JSONError'
@ -114,6 +115,19 @@ declare module 'solc' {
formattedMessage?: string;
}
import { ContractAbi } from '@0xproject/types';
export interface StandardContractOutput {
abi: ContractAbi;
evm: {
bytecode: {
object: string;
sourceMap: string;
};
deployedBytecode: {
object: string;
sourceMap: string;
};
};
}
export interface StandardOutput {
errors: Error[];
sources: {
@ -125,19 +139,7 @@ declare module 'solc' {
};
contracts: {
[fileName: string]: {
[contractName: string]: {
abi: ContractAbi;
evm: {
bytecode: {
object: string;
sourceMap: string;
};
deployedBytecode: {
object: string;
sourceMap: string;
};
};
};
[contractName: string]: StandardContractOutput;
};
};
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -154,7 +154,6 @@ export class LegacyPortal extends React.Component<LegacyPortalProps, LegacyPorta
const updateShouldBlockchainErrDialogBeOpen = this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen.bind(
this.props.dispatcher,
);
const isDevelopment = configs.ENVIRONMENT === Environments.DEVELOPMENT;
const portalStyle: React.CSSProperties = {
minHeight: '100vh',
display: 'flex',
@ -204,18 +203,6 @@ export class LegacyPortal extends React.Component<LegacyPortalProps, LegacyPorta
<div className="py2" style={{ backgroundColor: colors.grey50 }}>
{this.props.blockchainIsLoaded ? (
<Switch>
{isDevelopment && (
<Route
path={`${WebsitePaths.Portal}/wallet`}
render={this._renderWallet.bind(this)}
/>
)}
{isDevelopment && (
<Route
path={`${WebsitePaths.Portal}/relayers`}
render={this._renderRelayers.bind(this)}
/>
)}
<Route
path={`${WebsitePaths.Portal}/weth`}
render={this._renderEthWrapper.bind(this)}
@ -294,40 +281,6 @@ export class LegacyPortal extends React.Component<LegacyPortalProps, LegacyPorta
isLedgerDialogOpen: !this.state.isLedgerDialogOpen,
});
}
private _renderWallet() {
const allTokens = _.values(this.props.tokenByAddress);
const trackedTokens = _.filter(allTokens, t => t.isTracked);
return (
<div className="flex flex-center">
<div className="mx-auto">
<Wallet
userAddress={this.props.userAddress}
networkId={this.props.networkId}
blockchain={this._blockchain}
blockchainIsLoaded={this.props.blockchainIsLoaded}
blockchainErr={this.props.blockchainErr}
dispatcher={this.props.dispatcher}
tokenByAddress={this.props.tokenByAddress}
trackedTokens={trackedTokens}
userEtherBalanceInWei={this.props.userEtherBalanceInWei}
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
injectedProviderName={this.props.injectedProviderName}
providerType={this.props.providerType}
onToggleLedgerDialog={this.onToggleLedgerDialog.bind(this)}
/>
</div>
</div>
);
}
private _renderRelayers() {
return (
<div className="flex flex-center">
<div className="mx-auto" style={{ width: 800 }}>
<RelayerIndex networkId={this.props.networkId} />
</div>
</div>
);
}
private _renderEthWrapper() {
return (
<EthWrappers

View File

@ -58,26 +58,6 @@ export class LegacyPortalMenu extends React.Component<LegacyPortalMenuProps, Leg
>
{this._renderMenuItemWithIcon('Wrap ETH', 'zmdi-circle-o')}
</MenuItem>
{configs.ENVIRONMENT === Environments.DEVELOPMENT && (
<div>
<MenuItem
style={this.props.menuItemStyle}
className="py2"
to={`${WebsitePaths.Portal}/wallet`}
onClick={this.props.onClick.bind(this)}
>
{this._renderMenuItemWithIcon('Wallet', 'zmdi-balance-wallet')}
</MenuItem>
<MenuItem
style={this.props.menuItemStyle}
className="py2"
to={`${WebsitePaths.Portal}/relayers`}
onClick={this.props.onClick.bind(this)}
>
{this._renderMenuItemWithIcon('Relayers', 'zmdi-input-antenna')}
</MenuItem>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,263 @@
import { colors, Styles } from '@0xproject/react-shared';
import { BigNumber } from '@0xproject/utils';
import * as _ from 'lodash';
import * as React from 'react';
import * as DocumentTitle from 'react-document-title';
import { Blockchain } from 'ts/blockchain';
import { BlockchainErrDialog } from 'ts/components/dialogs/blockchain_err_dialog';
import { LedgerConfigDialog } from 'ts/components/dialogs/ledger_config_dialog';
import { PortalDisclaimerDialog } from 'ts/components/dialogs/portal_disclaimer_dialog';
import { AssetPicker } from 'ts/components/generate_order/asset_picker';
import { RelayerIndex } from 'ts/components/relayer_index/relayer_index';
import { TopBar, TopBarDisplayType } from 'ts/components/top_bar/top_bar';
import { FlashMessage } from 'ts/components/ui/flash_message';
import { Wallet } from 'ts/components/wallet/wallet';
import { localStorage } from 'ts/local_storage/local_storage';
import { Dispatcher } from 'ts/redux/dispatcher';
import { BlockchainErrs, HashData, Order, ProviderType, ScreenWidths, TokenByAddress, TokenVisibility } from 'ts/types';
import { constants } from 'ts/utils/constants';
import { Translate } from 'ts/utils/translate';
import { utils } from 'ts/utils/utils';
export interface PortalProps {
blockchainErr: BlockchainErrs;
blockchainIsLoaded: boolean;
dispatcher: Dispatcher;
hashData: HashData;
injectedProviderName: string;
networkId: number;
nodeVersion: string;
orderFillAmount: BigNumber;
providerType: ProviderType;
screenWidth: ScreenWidths;
tokenByAddress: TokenByAddress;
userEtherBalanceInWei: BigNumber;
userAddress: string;
shouldBlockchainErrDialogBeOpen: boolean;
userSuppliedOrderCache: Order;
location: Location;
flashMessage?: string | React.ReactNode;
lastForceTokenStateRefetch: number;
translate: Translate;
}
interface PortalState {
prevNetworkId: number;
prevNodeVersion: string;
prevUserAddress: string;
prevPathname: string;
isDisclaimerDialogOpen: boolean;
isLedgerDialogOpen: boolean;
isAssetPickerDialogOpen: boolean;
}
const THROTTLE_TIMEOUT = 100;
const TOP_BAR_HEIGHT = TopBar.heightForDisplayType(TopBarDisplayType.Expanded);
const styles: Styles = {
root: {
width: '100%',
height: '100%',
backgroundColor: colors.lightestGrey,
},
body: {
height: `calc(100vh - ${TOP_BAR_HEIGHT}px)`,
},
scrollContainer: {
height: `calc(100vh - ${TOP_BAR_HEIGHT}px)`,
WebkitOverflowScrolling: 'touch',
overflow: 'auto',
},
title: {
fontWeight: 'bold',
fontSize: 20,
},
};
export class Portal extends React.Component<PortalProps, PortalState> {
private _blockchain: Blockchain;
private _throttledScreenWidthUpdate: () => void;
constructor(props: PortalProps) {
super(props);
this._throttledScreenWidthUpdate = _.throttle(this._updateScreenWidth.bind(this), THROTTLE_TIMEOUT);
const didAcceptPortalDisclaimer = localStorage.getItemIfExists(constants.LOCAL_STORAGE_KEY_ACCEPT_DISCLAIMER);
const hasAcceptedDisclaimer =
!_.isUndefined(didAcceptPortalDisclaimer) && !_.isEmpty(didAcceptPortalDisclaimer);
this.state = {
prevNetworkId: this.props.networkId,
prevNodeVersion: this.props.nodeVersion,
prevUserAddress: this.props.userAddress,
prevPathname: this.props.location.pathname,
isDisclaimerDialogOpen: !hasAcceptedDisclaimer,
isAssetPickerDialogOpen: false,
isLedgerDialogOpen: false,
};
}
public componentDidMount() {
window.addEventListener('resize', this._throttledScreenWidthUpdate);
window.scrollTo(0, 0);
}
public componentWillMount() {
this._blockchain = new Blockchain(this.props.dispatcher);
}
public componentWillUnmount() {
this._blockchain.destroy();
window.removeEventListener('resize', this._throttledScreenWidthUpdate);
// We re-set the entire redux state when the portal is unmounted so that when it is re-rendered
// the initialization process always occurs from the same base state. This helps avoid
// initialization inconsistencies (i.e While the portal was unrendered, the user might have
// become disconnected from their backing Ethereum node, changed user accounts, etc...)
this.props.dispatcher.resetState();
}
public componentWillReceiveProps(nextProps: PortalProps) {
if (nextProps.networkId !== this.state.prevNetworkId) {
// tslint:disable-next-line:no-floating-promises
this._blockchain.networkIdUpdatedFireAndForgetAsync(nextProps.networkId);
this.setState({
prevNetworkId: nextProps.networkId,
});
}
if (nextProps.userAddress !== this.state.prevUserAddress) {
const newUserAddress = _.isEmpty(nextProps.userAddress) ? undefined : nextProps.userAddress;
// tslint:disable-next-line:no-floating-promises
this._blockchain.userAddressUpdatedFireAndForgetAsync(newUserAddress);
this.setState({
prevUserAddress: nextProps.userAddress,
});
}
if (nextProps.nodeVersion !== this.state.prevNodeVersion) {
// tslint:disable-next-line:no-floating-promises
this._blockchain.nodeVersionUpdatedFireAndForgetAsync(nextProps.nodeVersion);
}
if (nextProps.location.pathname !== this.state.prevPathname) {
this.setState({
prevPathname: nextProps.location.pathname,
});
}
}
public render() {
const updateShouldBlockchainErrDialogBeOpen = this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen.bind(
this.props.dispatcher,
);
const allTokens = _.values(this.props.tokenByAddress);
const trackedTokens = _.filter(allTokens, t => t.isTracked);
return (
<div style={styles.root}>
<DocumentTitle title="0x Portal DApp" />
<TopBar
userAddress={this.props.userAddress}
networkId={this.props.networkId}
injectedProviderName={this.props.injectedProviderName}
onToggleLedgerDialog={this._onToggleLedgerDialog.bind(this)}
dispatcher={this.props.dispatcher}
providerType={this.props.providerType}
blockchainIsLoaded={this.props.blockchainIsLoaded}
location={this.props.location}
blockchain={this._blockchain}
translate={this.props.translate}
displayType={TopBarDisplayType.Expanded}
style={{ backgroundColor: colors.lightestGrey }}
/>
<div id="portal" style={styles.body}>
<div className="sm-flex flex-center">
<div className="flex-last px3">
<div className="py3" style={styles.title}>
Your Account
</div>
<Wallet
userAddress={this.props.userAddress}
networkId={this.props.networkId}
blockchain={this._blockchain}
blockchainIsLoaded={this.props.blockchainIsLoaded}
blockchainErr={this.props.blockchainErr}
dispatcher={this.props.dispatcher}
tokenByAddress={this.props.tokenByAddress}
trackedTokens={trackedTokens}
userEtherBalanceInWei={this.props.userEtherBalanceInWei}
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
injectedProviderName={this.props.injectedProviderName}
providerType={this.props.providerType}
onToggleLedgerDialog={this._onToggleLedgerDialog.bind(this)}
onAddToken={this._onAddToken.bind(this)}
/>
</div>
<div className="flex-auto px3" style={styles.scrollContainer}>
<div className="py3" style={styles.title}>
Explore 0x Ecosystem
</div>
<RelayerIndex networkId={this.props.networkId} />
</div>
</div>
<BlockchainErrDialog
blockchain={this._blockchain}
blockchainErr={this.props.blockchainErr}
isOpen={this.props.shouldBlockchainErrDialogBeOpen}
userAddress={this.props.userAddress}
toggleDialogFn={updateShouldBlockchainErrDialogBeOpen}
networkId={this.props.networkId}
/>
<PortalDisclaimerDialog
isOpen={this.state.isDisclaimerDialogOpen}
onToggleDialog={this._onPortalDisclaimerAccepted.bind(this)}
/>
<FlashMessage dispatcher={this.props.dispatcher} flashMessage={this.props.flashMessage} />
{this.props.blockchainIsLoaded && (
<LedgerConfigDialog
providerType={this.props.providerType}
networkId={this.props.networkId}
blockchain={this._blockchain}
dispatcher={this.props.dispatcher}
toggleDialogFn={this._onToggleLedgerDialog.bind(this)}
isOpen={this.state.isLedgerDialogOpen}
/>
)}
<AssetPicker
userAddress={this.props.userAddress}
networkId={this.props.networkId}
blockchain={this._blockchain}
dispatcher={this.props.dispatcher}
isOpen={this.state.isAssetPickerDialogOpen}
currentTokenAddress={''}
onTokenChosen={this._onTokenChosen.bind(this)}
tokenByAddress={this.props.tokenByAddress}
tokenVisibility={TokenVisibility.UNTRACKED}
/>
</div>
</div>
);
}
private _onTokenChosen(tokenAddress: string) {
if (_.isEmpty(tokenAddress)) {
this.setState({
isAssetPickerDialogOpen: false,
});
return;
}
const token = this.props.tokenByAddress[tokenAddress];
this.props.dispatcher.updateTokenByAddress([token]);
this.setState({
isAssetPickerDialogOpen: false,
});
}
private _onToggleLedgerDialog() {
this.setState({
isLedgerDialogOpen: !this.state.isLedgerDialogOpen,
});
}
private _onAddToken() {
this.setState({
isAssetPickerDialogOpen: !this.state.isAssetPickerDialogOpen,
});
}
private _onPortalDisclaimerAccepted() {
localStorage.setItem(constants.LOCAL_STORAGE_KEY_ACCEPT_DISCLAIMER, 'set');
this.setState({
isDisclaimerDialogOpen: false,
});
}
private _updateScreenWidth() {
const newScreenWidth = utils.getScreenWidth();
this.props.dispatcher.updateScreenWidth(newScreenWidth);
}
}

View File

@ -12,37 +12,6 @@ export interface RelayerGridTileProps {
networkId: number;
}
// TODO: Get top tokens from remote
const topTokens = [
{
address: '0x1dad4783cf3fe3085c1426157ab175a6119a04ba',
decimals: 18,
iconUrl: '/images/token_icons/makerdao.png',
isRegistered: true,
isTracked: true,
name: 'Maker DAO',
symbol: 'MKR',
},
{
address: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
decimals: 18,
iconUrl: '/images/token_icons/melon.png',
isRegistered: true,
isTracked: true,
name: 'Melon Token',
symbol: 'MLN',
},
{
address: '0xb18845c260f680d5b9d84649638813e342e4f8c9',
decimals: 18,
iconUrl: '/images/token_icons/augur.png',
isRegistered: true,
isTracked: true,
name: 'Augur Reputation Token',
symbol: 'REP',
},
];
const styles: Styles = {
root: {
backgroundColor: colors.white,
@ -93,28 +62,63 @@ const styles: Styles = {
},
};
const FALLBACK_IMG_SRC = '/images/landing/hero_chip_image.png';
export const RelayerGridTile: React.StatelessComponent<RelayerGridTileProps> = (props: RelayerGridTileProps) => {
const link = props.relayerInfo.appUrl || props.relayerInfo.url;
return (
<a href={link} target="_blank" style={{ textDecoration: 'none' }}>
<GridTile style={styles.root}>
<div style={styles.innerDiv}>
<img src={props.relayerInfo.headerImgUrl} style={styles.header} />
<div style={styles.body}>
<div className="py1" style={styles.relayerNameLabel}>
{props.relayerInfo.name}
</div>
<div style={styles.dailyTradeVolumeLabel}>{props.relayerInfo.dailyTxnVolume}</div>
<div className="py1" style={styles.subLabel}>
Daily Trade Volume
</div>
<TopTokens tokens={topTokens} networkId={props.networkId} />
<div className="py1" style={styles.subLabel}>
Top tokens
</div>
<GridTile style={styles.root}>
<div style={styles.innerDiv}>
<a href={link} target="_blank" style={{ textDecoration: 'none' }}>
<ImgWithFallback
src={props.relayerInfo.headerImgUrl}
fallbackSrc={FALLBACK_IMG_SRC}
style={styles.header}
/>
</a>
<div style={styles.body}>
<div className="py1" style={styles.relayerNameLabel}>
{props.relayerInfo.name}
</div>
<div style={styles.dailyTradeVolumeLabel}>{props.relayerInfo.dailyTxnVolume}</div>
<div className="py1" style={styles.subLabel}>
Daily Trade Volume
</div>
<TopTokens tokens={props.relayerInfo.topTokens} networkId={props.networkId} />
<div className="py1" style={styles.subLabel}>
Top tokens
</div>
</div>
</GridTile>
</a>
</div>
</GridTile>
);
};
interface ImgWithFallbackProps {
src?: string;
fallbackSrc: string;
style: React.CSSProperties;
}
interface ImgWithFallbackState {
imageLoadFailed: boolean;
}
class ImgWithFallback extends React.Component<ImgWithFallbackProps, ImgWithFallbackState> {
constructor(props: ImgWithFallbackProps) {
super(props);
this.state = {
imageLoadFailed: false,
};
}
public render() {
if (this.state.imageLoadFailed || _.isUndefined(this.props.src)) {
return <img src={this.props.fallbackSrc} style={this.props.style} />;
} else {
return <img src={this.props.src} onError={this._onError.bind(this)} style={this.props.style} />;
}
}
private _onError() {
this.setState({
imageLoadFailed: true,
});
}
}

View File

@ -1,5 +1,7 @@
import { colors, Styles } from '@0xproject/react-shared';
import * as _ from 'lodash';
import CircularProgress from 'material-ui/CircularProgress';
import FlatButton from 'material-ui/FlatButton';
import { GridList } from 'material-ui/GridList';
import * as React from 'react';
@ -32,9 +34,9 @@ const styles: Styles = {
},
};
const CELL_HEIGHT = 260;
const CELL_HEIGHT = 290;
const NUMBER_OF_COLUMNS = 4;
const GRID_PADDING = 16;
const GRID_PADDING = 20;
export class RelayerIndex extends React.Component<RelayerIndexProps, RelayerIndexState> {
private _isUnmounted: boolean;
@ -55,7 +57,24 @@ export class RelayerIndex extends React.Component<RelayerIndexProps, RelayerInde
}
public render() {
const readyToRender = _.isUndefined(this.state.error) && !_.isUndefined(this.state.relayerInfos);
if (readyToRender) {
if (!readyToRender) {
return (
<div className="col col-12" style={{ ...styles.root, height: '100%' }}>
<div
className="relative sm-px2 sm-pt2 sm-m1"
style={{ height: 122, top: '33%', transform: 'translateY(-50%)' }}
>
<div className="center pb2">
{_.isUndefined(this.state.error) ? (
<CircularProgress size={40} thickness={5} />
) : (
<Retry onRetry={this._fetchRelayerInfosAsync.bind(this)} />
)}
</div>
</div>
</div>
);
} else {
return (
<div style={styles.root}>
<GridList
@ -64,23 +83,22 @@ export class RelayerIndex extends React.Component<RelayerIndexProps, RelayerInde
padding={GRID_PADDING}
style={styles.gridList}
>
{this.state.relayerInfos.map((relayerInfo: WebsiteBackendRelayerInfo) => (
<RelayerGridTile
key={relayerInfo.name}
relayerInfo={relayerInfo}
networkId={this.props.networkId}
/>
{this.state.relayerInfos.map((relayerInfo: WebsiteBackendRelayerInfo, index) => (
<RelayerGridTile key={index} relayerInfo={relayerInfo} networkId={this.props.networkId} />
))}
</GridList>
</div>
);
} else {
// TODO: loading and error states with a scrolling container
return null;
}
}
private async _fetchRelayerInfosAsync(): Promise<void> {
try {
if (!this._isUnmounted) {
this.setState({
relayerInfos: undefined,
error: undefined,
});
}
const relayerInfos = await backendClient.getRelayerInfosAsync();
if (!this._isUnmounted) {
this.setState({
@ -96,3 +114,31 @@ export class RelayerIndex extends React.Component<RelayerIndexProps, RelayerInde
}
}
}
interface RetryProps {
onRetry: () => void;
}
const Retry = (props: RetryProps) => (
<div className="clearfix center" style={{ color: colors.black }}>
<div className="mx-auto inline-block align-middle" style={{ lineHeight: '44px', textAlign: 'center' }}>
<div className="h2" style={{ fontFamily: 'Roboto Mono' }}>
Something went wrong.
</div>
<div className="py3">
<FlatButton
label={'reload'}
backgroundColor={colors.black}
labelStyle={{
fontSize: 18,
fontFamily: 'Roboto Mono',
fontWeight: 'lighter',
color: colors.white,
textTransform: 'lowercase',
}}
style={{ width: 280, height: 62, borderRadius: 5 }}
onClick={props.onRetry}
/>
</div>
</div>
</div>
);

View File

@ -3,10 +3,10 @@ import * as _ from 'lodash';
import * as React from 'react';
import { TokenIcon } from 'ts/components/ui/token_icon';
import { Token } from 'ts/types';
import { WebsiteBackendTokenInfo } from 'ts/types';
export interface TopTokensProps {
tokens: Token[];
tokens: WebsiteBackendTokenInfo[];
networkId: number;
}
@ -23,17 +23,17 @@ const styles: Styles = {
export const TopTokens: React.StatelessComponent<TopTokensProps> = (props: TopTokensProps) => {
return (
<div className="flex">
{_.map(props.tokens, (token: Token, index: number) => {
{_.map(props.tokens, (tokenInfo: WebsiteBackendTokenInfo, index: number) => {
const firstItemStyle = { ...styles.tokenLabel, ...styles.followingTokenLabel };
const style = index !== 0 ? firstItemStyle : styles.tokenLabel;
return (
<a
key={token.address}
href={tokenLinkFromToken(token, props.networkId)}
key={tokenInfo.address}
href={tokenLinkFromToken(tokenInfo, props.networkId)}
target="_blank"
style={style}
>
{token.symbol}
{tokenInfo.symbol}
</a>
);
})}
@ -41,6 +41,6 @@ export const TopTokens: React.StatelessComponent<TopTokensProps> = (props: TopTo
);
};
function tokenLinkFromToken(token: Token, networkId: number) {
return sharedUtils.getEtherScanLinkIfExists(token.address, networkId, EtherscanLinkSuffixes.Address);
function tokenLinkFromToken(tokenInfo: WebsiteBackendTokenInfo, networkId: number) {
return sharedUtils.getEtherScanLinkIfExists(tokenInfo.address, networkId, EtherscanLinkSuffixes.Address);
}

View File

@ -1,4 +1,4 @@
import { colors } from '@0xproject/react-shared';
import { colors, Styles } from '@0xproject/react-shared';
import * as _ from 'lodash';
import RaisedButton from 'material-ui/RaisedButton';
import * as React from 'react';
@ -11,9 +11,9 @@ import { ProviderType } from 'ts/types';
import { constants } from 'ts/utils/constants';
import { utils } from 'ts/utils/utils';
const IDENTICON_DIAMETER = 32;
const ROOT_HEIGHT = 24;
interface ProviderDisplayProps {
export interface ProviderDisplayProps {
dispatcher: Dispatcher;
userAddress: string;
networkId: number;
@ -25,6 +25,15 @@ interface ProviderDisplayProps {
interface ProviderDisplayState {}
const styles: Styles = {
root: {
height: ROOT_HEIGHT,
backgroundColor: colors.white,
borderRadius: ROOT_HEIGHT,
boxShadow: `0px 4px 6px ${colors.walletBoxShadow}`,
},
};
export class ProviderDisplay extends React.Component<ProviderDisplayProps, ProviderDisplayState> {
public render() {
const isAddressAvailable = !_.isEmpty(this.props.userAddress);
@ -40,21 +49,20 @@ export class ProviderDisplay extends React.Component<ProviderDisplayProps, Provi
: 'Connect a wallet';
const providerTitle =
this.props.providerType === ProviderType.Injected ? injectedProviderName : 'Ledger Nano S';
const isProviderMetamask = providerTitle === constants.PROVIDER_NAME_METAMASK;
const hoverActiveNode = (
<div className="flex right lg-pr0 md-pr2 sm-pr2" style={{ paddingTop: 16 }}>
<div className="flex right lg-pr0 md-pr2 sm-pr2 p1" style={styles.root}>
<div>
<Identicon address={this.props.userAddress} diameter={IDENTICON_DIAMETER} />
<Identicon address={this.props.userAddress} diameter={ROOT_HEIGHT} />
</div>
<div style={{ marginLeft: 12, paddingTop: 1 }}>
<div style={{ fontSize: 12, color: colors.amber800 }}>{providerTitle}</div>
<div style={{ fontSize: 14 }}>{displayAddress}</div>
</div>
<div
style={{ borderLeft: `1px solid ${colors.grey300}`, marginLeft: 17, paddingTop: 1 }}
className="px2"
>
<i style={{ fontSize: 30, color: colors.grey300 }} className="zmdi zmdi zmdi-chevron-down" />
<div style={{ marginLeft: 12, paddingTop: 3 }}>
<div style={{ fontSize: 16, color: colors.darkGrey }}>{displayAddress}</div>
</div>
{isProviderMetamask && (
<div style={{ marginLeft: 16 }}>
<img src="/images/metamask_icon.png" style={{ width: ROOT_HEIGHT, height: ROOT_HEIGHT }} />
</div>
)}
</div>
);
const hasInjectedProvider =

View File

@ -19,7 +19,12 @@ import { Deco, Key, ProviderType, WebsiteLegacyPaths, WebsitePaths } from 'ts/ty
import { constants } from 'ts/utils/constants';
import { Translate } from 'ts/utils/translate';
interface TopBarProps {
export enum TopBarDisplayType {
Default,
Expanded,
}
export interface TopBarProps {
userAddress?: string;
networkId?: number;
injectedProviderName?: string;
@ -34,7 +39,7 @@ interface TopBarProps {
availableDocVersions?: string[];
menu?: DocsMenu;
menuSubsectionsBySection?: MenuSubsectionsBySection;
shouldFullWidth?: boolean;
displayType?: TopBarDisplayType;
docsInfo?: DocsInfo;
style?: React.CSSProperties;
isNightVersion?: boolean;
@ -47,17 +52,8 @@ interface TopBarState {
}
const styles: Styles = {
address: {
marginRight: 12,
overflow: 'hidden',
paddingTop: 4,
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
width: 70,
},
topBar: {
backgroundColor: colors.white,
height: 59,
width: '100%',
position: 'relative',
top: 0,
@ -78,12 +74,19 @@ const styles: Styles = {
},
};
const DEFAULT_HEIGHT = 59;
const EXPANDED_HEIGHT = 75;
export class TopBar extends React.Component<TopBarProps, TopBarState> {
public static defaultProps: Partial<TopBarProps> = {
shouldFullWidth: false,
displayType: TopBarDisplayType.Default,
style: {},
isNightVersion: false,
};
public static heightForDisplayType(displayType: TopBarDisplayType) {
const result = displayType === TopBarDisplayType.Expanded ? EXPANDED_HEIGHT : DEFAULT_HEIGHT;
return result + 1;
}
constructor(props: TopBarProps) {
super(props);
this.state = {
@ -92,8 +95,9 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> {
}
public render() {
const isNightVersion = this.props.isNightVersion;
const isFullWidthPage = this.props.shouldFullWidth;
const parentClassNames = `flex mx-auto ${isFullWidthPage ? 'pl2' : 'max-width-4'}`;
const isExpandedDisplayType = this.props.displayType === TopBarDisplayType.Expanded;
const parentClassNames = `flex mx-auto ${isExpandedDisplayType ? 'pl3 py1' : 'max-width-4'}`;
const height = isExpandedDisplayType ? EXPANDED_HEIGHT : DEFAULT_HEIGHT;
const developerSectionMenuItems = [
<Link key="subMenuItem-zeroEx" to={WebsitePaths.ZeroExJs} className="text-decoration-none">
<MenuItem style={{ fontSize: styles.menuItem.fontSize }} primaryText="0x.js" />
@ -178,9 +182,11 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> {
</a>,
];
const bottomBorderStyle = this._shouldDisplayBottomBar() ? styles.bottomBar : {};
const fullWidthClasses = isFullWidthPage ? 'pr4' : '';
const fullWidthClasses = isExpandedDisplayType ? 'pr4' : '';
const logoUrl = isNightVersion ? '/images/protocol_logo_white.png' : '/images/protocol_logo_black.png';
const menuClasses = `col col-${isFullWidthPage ? '4' : '5'} ${fullWidthClasses} lg-pr0 md-pr2 sm-hide xs-hide`;
const menuClasses = `col col-${
isExpandedDisplayType ? '4' : '5'
} ${fullWidthClasses} lg-pr0 md-pr2 sm-hide xs-hide`;
const menuIconStyle = {
fontSize: 25,
color: isNightVersion ? 'white' : 'black',
@ -197,15 +203,15 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> {
);
const popoverContent = <Menu style={{ color: colors.darkGrey }}>{developerSectionMenuItems}</Menu>;
return (
<div style={{ ...styles.topBar, ...bottomBorderStyle, ...this.props.style }} className="pb1">
<div style={{ ...styles.topBar, ...bottomBorderStyle, ...this.props.style, ...{ height } }} className="pb1">
<div className={parentClassNames}>
<div className="col col-2 sm-pl1 md-pl2 lg-pl0" style={{ paddingTop: 15 }}>
<Link to={`${WebsitePaths.Home}`} className="text-decoration-none">
<img src={logoUrl} height="30" />
</Link>
</div>
<div className={`col col-${isFullWidthPage ? '8' : '9'} lg-hide md-hide`} />
<div className={`col col-${isFullWidthPage ? '6' : '5'} sm-hide xs-hide`} />
<div className={`col col-${isExpandedDisplayType ? '8' : '9'} lg-hide md-hide`} />
<div className={`col col-${isExpandedDisplayType ? '6' : '5'} sm-hide xs-hide`} />
{!this._isViewingPortal() && (
<div className={menuClasses}>
<div className="flex justify-between">
@ -242,7 +248,7 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> {
path={`${WebsitePaths.Portal}`}
isPrimary={true}
style={styles.menuItem}
className={`${isFullWidthPage && 'md-hide'}`}
className={`${isExpandedDisplayType && 'md-hide'}`}
isNightVersion={isNightVersion}
isExternal={false}
/>
@ -250,7 +256,7 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> {
</div>
)}
{this.props.blockchainIsLoaded && (
<div className="sm-hide xs-hide col col-5">
<div className="sm-hide xs-hide col col-5" style={{ paddingTop: 8, marginRight: 36 }}>
<ProviderDisplay
dispatcher={this.props.dispatcher}
userAddress={this.props.userAddress}
@ -262,7 +268,7 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> {
/>
</div>
)}
<div className={`col ${isFullWidthPage ? 'col-2 pl2' : 'col-1'} md-hide lg-hide`}>
<div className={`col ${isExpandedDisplayType ? 'col-2 pl2' : 'col-1'} md-hide lg-hide`}>
<div style={menuIconStyle}>
<i className="zmdi zmdi-menu" onClick={this._onMenuButtonClick.bind(this)} />
</div>
@ -441,21 +447,6 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> {
</div>
);
}
private _renderUser() {
const userAddress = this.props.userAddress;
const identiconDiameter = 26;
return (
<div className="flex right lg-pr0 md-pr2 sm-pr2" style={{ paddingTop: 16 }}>
<div style={styles.address} data-tip={true} data-for="userAddressTooltip">
{!_.isEmpty(userAddress) ? userAddress : ''}
</div>
<ReactTooltip id="userAddressTooltip">{userAddress}</ReactTooltip>
<div>
<Identicon address={userAddress} diameter={identiconDiameter} />
</div>
</div>
);
}
private _onMenuButtonClick() {
this.setState({
isDrawerOpen: !this.state.isDrawerOpen,
@ -511,7 +502,8 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> {
this._isViewingJsonSchemasDocs() ||
this._isViewingSolCovDocs() ||
this._isViewingSubprovidersDocs() ||
this._isViewingConnectDocs()
this._isViewingConnectDocs() ||
this._isViewingPortal()
);
}
} // tslint:disable:max-file-line-count

View File

@ -55,11 +55,13 @@ export interface WalletProps {
injectedProviderName: string;
providerType: ProviderType;
onToggleLedgerDialog: () => void;
onAddToken: () => void;
}
interface WalletState {
trackedTokenStateByAddress: TokenStateByAddress;
wrappedEtherDirection?: Side;
isHoveringSidebar: boolean;
}
interface AllowanceToggleConfig {
@ -94,6 +96,9 @@ const styles: Styles = {
},
footerItemInnerDiv: {
paddingLeft: 24,
borderTopColor: colors.walletBorder,
borderTopStyle: 'solid',
borderWidth: 1,
},
borderedItem: {
borderBottomColor: colors.walletBorder,
@ -114,7 +119,17 @@ const styles: Styles = {
paddingTop: 8,
paddingBottom: 8,
},
accessoryItemsContainer: { width: 150, right: 8 },
accessoryItemsContainer: {
width: 150,
right: 8,
},
bodyInnerDiv: {
padding: 0,
// TODO: make this completely responsive
maxHeight: 475,
overflow: 'auto',
WebkitOverflowScrolling: 'touch',
},
};
const ETHER_ICON_PATH = '/images/ether.png';
@ -139,6 +154,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
this.state = {
trackedTokenStateByAddress: initialTrackedTokenStateByAddress,
wrappedEtherDirection: undefined,
isHoveringSidebar: false,
};
}
public componentWillMount() {
@ -184,12 +200,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
return (
<List style={styles.list}>
{isAddressAvailable
? _.concat(
this._renderConnectedHeaderRows(),
this._renderEthRows(),
this._renderTokenRows(),
this._renderFooterRows(),
)
? _.concat(this._renderConnectedHeaderRows(), this._renderBody(), this._renderFooterRows())
: _.concat(this._renderDisconnectedHeaderRows(), this._renderDisconnectedRows())}
</List>
);
@ -230,9 +241,43 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
/>
);
}
private _renderBody() {
const bodyStyle: React.CSSProperties = {
...styles.bodyInnerDiv,
overflow: this.state.isHoveringSidebar ? 'auto' : 'hidden',
};
return (
<ListItem
key="body"
innerDivStyle={bodyStyle}
onMouseEnter={this._onSidebarHover.bind(this)}
onMouseLeave={this._onSidebarHoverOff.bind(this)}
>
{this._renderEthRows()}
{this._renderTokenRows()}
</ListItem>
);
}
private _onSidebarHover(event: React.FormEvent<HTMLInputElement>) {
this.setState({
isHoveringSidebar: true,
});
}
private _onSidebarHoverOff() {
this.setState({
isHoveringSidebar: false,
});
}
private _renderFooterRows() {
const primaryText = '+ other tokens';
return <ListItem key={FOOTER_ITEM_KEY} primaryText={primaryText} innerDivStyle={styles.footerItemInnerDiv} />;
return (
<ListItem
key={FOOTER_ITEM_KEY}
primaryText={primaryText}
innerDivStyle={styles.footerItemInnerDiv}
onClick={this.props.onAddToken}
/>
);
}
private _renderEthRows() {
const primaryText = this._renderAmount(
@ -293,7 +338,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
);
return _.map(trackedTokensStartingWithEtherToken, this._renderTokenRow.bind(this));
}
private _renderTokenRow(token: Token) {
private _renderTokenRow(token: Token, index: number) {
const tokenState = this.state.trackedTokenStateByAddress[token.address];
const tokenLink = sharedUtils.getEtherScanLinkIfExists(
token.address,
@ -310,12 +355,14 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
tokenState,
},
};
// if this is the last item in the list, do not render the border, it is rendered by the footer
const borderedStyle = index !== this.props.trackedTokens.length - 1 ? styles.borderedItem : {};
const shouldShowWrapEtherItem =
!_.isUndefined(this.state.wrappedEtherDirection) &&
this.state.wrappedEtherDirection === accessoryItemConfig.wrappedEtherDirection;
const style = shouldShowWrapEtherItem
? { ...walletItemStyles.focusedItem, ...styles.paddedItem }
: { ...styles.tokenItem, ...styles.borderedItem, ...styles.paddedItem };
: { ...styles.tokenItem, ...borderedStyle, ...styles.paddedItem };
const etherToken = this._getEthToken();
return (
<div key={token.address}>

View File

@ -0,0 +1,88 @@
import { BigNumber } from '@0xproject/utils';
import * as _ from 'lodash';
import * as React from 'react';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import { Portal as PortalComponent, PortalProps as PortalComponentProps } from 'ts/components/portal/portal';
import { Dispatcher } from 'ts/redux/dispatcher';
import { State } from 'ts/redux/reducer';
import { BlockchainErrs, HashData, Order, ProviderType, ScreenWidths, Side, TokenByAddress } from 'ts/types';
import { constants } from 'ts/utils/constants';
import { Translate } from 'ts/utils/translate';
interface ConnectedState {
blockchainErr: BlockchainErrs;
blockchainIsLoaded: boolean;
hashData: HashData;
injectedProviderName: string;
networkId: number;
nodeVersion: string;
orderFillAmount: BigNumber;
providerType: ProviderType;
tokenByAddress: TokenByAddress;
lastForceTokenStateRefetch: number;
userEtherBalanceInWei: BigNumber;
screenWidth: ScreenWidths;
shouldBlockchainErrDialogBeOpen: boolean;
userAddress: string;
userSuppliedOrderCache: Order;
flashMessage?: string | React.ReactNode;
translate: Translate;
}
interface ConnectedDispatch {
dispatcher: Dispatcher;
}
const mapStateToProps = (state: State, ownProps: PortalComponentProps): ConnectedState => {
const receiveAssetToken = state.sideToAssetToken[Side.Receive];
const depositAssetToken = state.sideToAssetToken[Side.Deposit];
const receiveAddress = !_.isUndefined(receiveAssetToken.address)
? receiveAssetToken.address
: constants.NULL_ADDRESS;
const depositAddress = !_.isUndefined(depositAssetToken.address)
? depositAssetToken.address
: constants.NULL_ADDRESS;
const receiveAmount = !_.isUndefined(receiveAssetToken.amount) ? receiveAssetToken.amount : new BigNumber(0);
const depositAmount = !_.isUndefined(depositAssetToken.amount) ? depositAssetToken.amount : new BigNumber(0);
const hashData = {
depositAmount,
depositTokenContractAddr: depositAddress,
feeRecipientAddress: constants.NULL_ADDRESS,
makerFee: constants.MAKER_FEE,
orderExpiryTimestamp: state.orderExpiryTimestamp,
orderMakerAddress: state.userAddress,
orderTakerAddress: state.orderTakerAddress !== '' ? state.orderTakerAddress : constants.NULL_ADDRESS,
receiveAmount,
receiveTokenContractAddr: receiveAddress,
takerFee: constants.TAKER_FEE,
orderSalt: state.orderSalt,
};
return {
blockchainErr: state.blockchainErr,
blockchainIsLoaded: state.blockchainIsLoaded,
hashData,
injectedProviderName: state.injectedProviderName,
networkId: state.networkId,
nodeVersion: state.nodeVersion,
orderFillAmount: state.orderFillAmount,
providerType: state.providerType,
screenWidth: state.screenWidth,
shouldBlockchainErrDialogBeOpen: state.shouldBlockchainErrDialogBeOpen,
tokenByAddress: state.tokenByAddress,
lastForceTokenStateRefetch: state.lastForceTokenStateRefetch,
userAddress: state.userAddress,
userEtherBalanceInWei: state.userEtherBalanceInWei,
userSuppliedOrderCache: state.userSuppliedOrderCache,
flashMessage: state.flashMessage,
translate: state.translate,
};
};
const mapDispatchToProps = (dispatch: Dispatch<State>): ConnectedDispatch => ({
dispatcher: new Dispatcher(dispatch),
});
export const Portal: React.ComponentClass<PortalComponentProps> = connect(mapStateToProps, mapDispatchToProps)(
PortalComponent,
);

View File

@ -34,9 +34,14 @@ import 'less/all.less';
// cause we only want to import the module when the user navigates to the page.
// At the same time webpack statically parses for System.import() to determine bundle chunk split points
// so each lazy import needs it's own `System.import()` declaration.
const LazyPortal = createLazyComponent('LegacyPortal', async () =>
System.import<any>(/* webpackChunkName: "legacyPortal" */ 'ts/containers/legacy_portal'),
);
const LazyPortal =
utils.isDevelopment() || utils.isStaging()
? createLazyComponent('Portal', async () =>
System.import<any>(/* webpackChunkName: "portal" */ 'ts/containers/portal'),
)
: createLazyComponent('LegacyPortal', async () =>
System.import<any>(/* webpackChunkName: "legacyPortal" */ 'ts/containers/legacy_portal'),
);
const LazyZeroExJSDocumentation = createLazyComponent('Documentation', async () =>
System.import<any>(/* webpackChunkName: "zeroExDocs" */ 'ts/containers/zero_ex_js_documentation'),
);

View File

@ -503,19 +503,26 @@ export interface TokenState {
price?: BigNumber;
}
// TODO: Add topTokens property once available from backend
export interface WebsiteBackendRelayerInfo {
name: string;
dailyTxnVolume: string;
url: string;
appUrl?: string;
headerImgUrl: string;
headerImgUrl?: string;
topTokens: WebsiteBackendTokenInfo[];
}
export interface WebsiteBackendPriceInfo {
[symbol: string]: string;
}
export interface WebsiteBackendTokenInfo {
address: string;
decimals: number;
name: string;
symbol: string;
}
export interface WebsiteBackendGasInfo {
average: number;
}