diff --git a/.flowconfig b/.flowconfig new file mode 100644 index 00000000..ab91d13d --- /dev/null +++ b/.flowconfig @@ -0,0 +1,20 @@ +[ignore] +.*/node_modules/bitcoinjs-lib-zcash/.* +.*/node_modules/bitcoinjs-lib/.* +.*/node_modules/hd-wallet/.* +.*/node_modules/protobufjs-old-fixed-webpack/src/bower.json +.*/node_modules/trezor-link/lib/.* +.*/_old/.* + +[libs] + + + +[options] +esproposal.class_static_fields=enable +esproposal.class_instance_fields=enable +esproposal.export_star_as=enable +suppress_comment=\\(.\\|\n\\)*\\$FlowIssue +esproposal.decorators=ignore +module.name_mapper='.*\(.less\)' -> 'CSSModule' +module.system=haste diff --git a/.gitignore b/.gitignore index 4fb43b65..1ecda50d 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,6 @@ logs .yarnclean # Local config file -webpack/constants.js \ No newline at end of file +webpack/constants.js + +_old \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100644 index 00000000..e071864d --- /dev/null +++ b/build.sh @@ -0,0 +1,13 @@ + + +printf "\n-- DEPLOY START -----------------------\n" + +yarn run build + +printf "\n-- COPYING FILES ----------------------\n" + +cd build +rsync -avz --delete -e ssh . admin@dev.sldev.cz:~/experiments/www +cd ../ + +printf "\n-- COMPLETE ---------------------------\n" diff --git a/images/bch-logo.png b/images/bch-logo.png new file mode 100644 index 00000000..9590c12e Binary files /dev/null and b/images/bch-logo.png differ diff --git a/images/btc-logo.png b/images/btc-logo.png new file mode 100644 index 00000000..5b77226c Binary files /dev/null and b/images/btc-logo.png differ diff --git a/images/btg-logo.png b/images/btg-logo.png new file mode 100644 index 00000000..5eda4570 Binary files /dev/null and b/images/btg-logo.png differ diff --git a/images/case.png b/images/case.png new file mode 100644 index 00000000..eee16f1d Binary files /dev/null and b/images/case.png differ diff --git a/images/dash-logo.png b/images/dash-logo.png new file mode 100644 index 00000000..5c3a113d Binary files /dev/null and b/images/dash-logo.png differ diff --git a/images/etc-logo.png b/images/etc-logo.png new file mode 100644 index 00000000..e868d6ea Binary files /dev/null and b/images/etc-logo.png differ diff --git a/images/eth-logo.png b/images/eth-logo.png new file mode 100644 index 00000000..5b7c1a8a Binary files /dev/null and b/images/eth-logo.png differ diff --git a/images/icontrezor.png b/images/icontrezor.png new file mode 100644 index 00000000..8c35659b Binary files /dev/null and b/images/icontrezor.png differ diff --git a/images/ltc-logo.png b/images/ltc-logo.png new file mode 100644 index 00000000..201f2350 Binary files /dev/null and b/images/ltc-logo.png differ diff --git a/images/zec-logo.png b/images/zec-logo.png new file mode 100644 index 00000000..134ba792 Binary files /dev/null and b/images/zec-logo.png differ diff --git a/package.json b/package.json index bfaf3057..55bc64da 100644 --- a/package.json +++ b/package.json @@ -21,16 +21,26 @@ }, "dependencies": { "babel-preset-react": "^6.24.1", + "color-hash": "^1.0.3", "ethereumjs-tx": "^1.3.3", + "ethereumjs-units": "^0.2.0", "ethereumjs-util": "^5.1.2", "hdkey": "0.7.1", + "path-to-regexp": "^2.1.0", + "raf": "^3.4.0", + "rc-tooltip": "^3.7.0", "react": "^16.1.1", + "react-blockies": "^1.2.2", + "react-css-transition": "^0.7.4", "react-dom": "^16.1.1", + "react-ellipsis-text": "^1.0.0", "react-hot-loader": "^3.1.3", "react-qr-svg": "^2.1.0", "react-redux": "^5.0.6", "react-router-dom": "^4.2.2", "react-router-redux": "next", + "react-scale-text": "^1.2.2", + "react-select": "^1.1.0", "react-transition-group": "^2.2.1", "redux": "^3.7.2", "redux-logger": "^3.0.6", diff --git a/src/assets/tos.pdf b/src/assets/tos.pdf new file mode 100644 index 00000000..501795a6 Binary files /dev/null and b/src/assets/tos.pdf differ diff --git a/src/data/appConfig.json b/src/data/appConfig.json new file mode 100644 index 00000000..2c9ce85b --- /dev/null +++ b/src/data/appConfig.json @@ -0,0 +1,81 @@ +{ + "coins1": [ + { + "name": "Ethereum Ropsten", + "symbol": "eth", + "network": "ropsten-eth", + "shortcut": "eth", + "bip44": "m/44'/60'/0'/0", + "defaultGasPrice": 64, + "defaultGasLimit": 21000, + "defaultGasLimitTokens": 200000, + "backends": [ + { + "name": "TREZOR Wallet - Ethereum", + "urls": [ + "https://ropsten.infura.io/QGyVKozSUEh2YhL4s2G4", + "http://10.34.2.5:8545" + ], + "explorer": "https://blockexplorer.com" + } + ] + } + ], + + "coins": [ + { + "name": "Ethereum Ropsten", + "symbol": "eth", + "network": "ropsten-eth", + "shortcut": "eth", + "bip44": "m/44'/60'/0'/0", + "defaultGasPrice": 64, + "defaultGasLimit": 21000, + "defaultGasLimitTokens": 200000, + "backends": [ + { + "name": "TREZOR Wallet - Ethereum", + "urls": [ + "https://ropsten.infura.io/QGyVKozSUEh2YhL4s2G4", + "http://10.34.2.5:8545" + ], + "explorer": "https://blockexplorer.com" + } + ] + }, + { + "name": "Ethereum Rinkeby", + "symbol": "etc", + "network": "ropsten-eth", + "shortcut": "etc", + "bip44": "m/44'/61'/0'/0", + "defaultGasPrice": 64, + "defaultGasLimit": 21000, + "defaultGasLimitTokens": 200000, + "backends": [ + { + "name": "TREZOR Wallet - Ethereum", + "urls": [ + "https://rinkeby.infura.io/QGyVKozSUEh2YhL4s2G4", + "http://10.34.2.5:8545" + ], + "explorer": "https://blockexplorer.com" + } + ] + } + ], + + "fiatValueTickers": [ + + ], + + "bridge": { + "url": "https://localback.net:21324", + "configUrl": "data/config_signed.bin", + "latestUrl": "data/bridge/latest.txt" + }, + "extensionId": "jcjjhjgimijdkoamemaghajlhegmoclj", + "storageVersion": "1.1.0", + "metadataVersion": "1.0.0" + +} \ No newline at end of file diff --git a/src/data/ethERC20.json b/src/data/ethERC20.json new file mode 100644 index 00000000..68cd8d8e --- /dev/null +++ b/src/data/ethERC20.json @@ -0,0 +1,152 @@ +[ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "name":"", + "type":"string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"name":"_spender", "type":"address"}, + {"name":"_value","type":"uint256"} + ], + "name":"approve", + "outputs":[ + {"name":"success","type":"bool"} + ], + "payable":false, + "stateMutability":"nonpayable", + "type":"function" + }, + { + "constant":true, + "inputs":[], + "name":"totalSupply", + "outputs":[ + {"name":"","type":"uint256"} + ], + "payable":false, + "stateMutability":"view", + "type":"function" + }, + { + "constant":false, + "inputs":[ + {"name":"_from","type":"address"}, + {"name":"_to","type":"address"}, + {"name":"_value","type":"uint256"} + ], + "name":"transferFrom", + "outputs":[ + {"name":"success","type":"bool"} + ], + "payable":false, + "stateMutability":"nonpayable", + "type":"function" + }, + { + "constant":true, + "inputs":[], + "name":"decimals", + "outputs":[ + {"name":"","type":"uint8"} + ], + "payable":false, + "stateMutability":"view", + "type":"function" + }, + { + "constant":true, + "inputs":[], + "name":"version", + "outputs":[ + {"name":"","type":"string"} + ], + "payable":false, + "stateMutability":"view", + "type":"function" + }, + {"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}, + {"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"}, + { + "constant":false, + "inputs":[ + {"name":"_to","type":"address"}, + {"name":"_value","type":"uint256"} + ], + "name":"transfer", + "outputs":[ + {"name":"success","type":"bool"} + ], + "payable":false, + "stateMutability":"nonpayable", + "type":"function" + }, + { + "constant":false, + "inputs":[ + {"name":"_spender","type":"address"}, + {"name":"_value","type":"uint256"}, + {"name":"_extraData","type":"bytes"} + ], + "name":"approveAndCall", + "outputs":[ + {"name":"success","type":"bool"} + ], + "payable":false, + "stateMutability":"nonpayable", + "type":"function" + }, + { + "constant":true, + "inputs":[ + {"name":"_owner","type":"address"}, + {"name":"_spender","type":"address"} + ], + "name":"allowance", + "outputs":[ + {"name":"remaining","type":"uint256"} + ], + "payable":false, + "stateMutability":"view", + "type":"function" + }, + { + "inputs":[], + "payable":false, + "stateMutability":"nonpayable", + "type":"constructor" + }, + { + "payable":false, + "stateMutability":"nonpayable", + "type":"fallback" + },{ + "anonymous":false, + "inputs":[ + {"indexed":true,"name":"_from","type":"address"}, + {"indexed":true,"name":"_to","type":"address"}, + {"indexed":false,"name":"_value","type":"uint256"} + ], + "name":"Transfer", + "type":"event" + },{ + "anonymous":false, + "inputs":[ + {"indexed":true,"name":"_owner","type":"address"}, + {"indexed":true,"name":"_spender","type":"address"}, + {"indexed":false,"name":"_value","type":"uint256"} + ], + "name":"Approval", + "type":"event" + } +] \ No newline at end of file diff --git a/src/data/ethTokens.json b/src/data/ethTokens.json new file mode 100644 index 00000000..4b95fd74 --- /dev/null +++ b/src/data/ethTokens.json @@ -0,0 +1,2504 @@ +[ + { + "address": "0xfdbc1adc26f0f8f8606a5d63b7d3a3cd21c22b23", + "name": "1World", + "symbol": "1WO", + "decimals": 8 + }, + { + "address": "0xAf30D2a7E90d7DC361c8C4585e9BB7D2F6f15bc7", + "name": "Firstblood", + "symbol": "1ST", + "decimals": 18 + }, + { + "address": "0xaEc98A708810414878c3BCDF46Aad31dEd4a4557", + "name": "300 Token", + "symbol": "300", + "decimals": 18 + }, + { + "address": "0x13f1b7fdfbe1fc66676d56483e21b1ecb40b58e2", + "name": "Accelerator", + "symbol": "ACC", + "decimals": 18 + }, + { + "address": "0x8810C63470d38639954c6B41AaC545848C46484a", + "name": "Aditus", + "symbol": "ADI", + "decimals": 18 + }, + { + "address": "0x422866a8F0b032c5cf1DfBDEf31A20F4509562b0", + "name": "Adshares Token", + "symbol": "ADST", + "decimals": 0 + }, + { + "address": "0xD0D6D6C5Fe4a677D343cC433536BB717bAe167dD", + "name": "AdToken", + "symbol": "ADT", + "decimals": 9 + }, + { + "address": "0x4470BB87d77b963A013DB939BE332f927f2b992e", + "name": "AdEx", + "symbol": "ADX", + "decimals": 4 + }, + { + "address": "0x27dce1ec4d3f72c3e457cc50354f1f975ddef488", + "name": "AirToken", + "symbol": "AIR", + "decimals": 8 + }, + { + "address": "0x1063ce524265d5a3A624f4914acd573dD89ce988", + "name": "Aigang", + "symbol": "AIX", + "decimals": 18 + }, + { + "address": "0xEA610B1153477720748DC13ED378003941d84fAB", + "name": "AlisToken", + "symbol": "ALIS", + "decimals": 18 + }, + { + "address": "0x181a63746d3adcf356cbc73ace22832ffbb1ee5a", + "name": "Alaricoin", + "symbol": "ALCO", + "decimals": 8 + }, + { + "address": "0x638ac149ea8ef9a1286c41b977017aa7359e6cfa", + "name": "Altcoins", + "symbol": "ALTS", + "decimals": 18 + }, + { + "address": "0x4dc3643dbc642b72c158e7f3d2ff232df61cb6ce", + "name": "Amber Token", + "symbol": "AMB", + "decimals": 18 + }, + { + "address": "0x949bed886c739f1a3273629b3320db0c5024c719", + "name": "AMIS", + "symbol": "AMIS", + "decimals": 9 + }, + { + "address": "0x960b236A07cf122663c4303350609A66A7B288C0", + "name": "Aragon Network Token", + "symbol": "ANT", + "decimals": 18 + }, + { + "address": "0x1a7a8bd9106f2b8d977e08582dc7d24c723ab0db", + "name": "AppCoins", + "symbol": "APPC", + "decimals": 18 + }, + { + "address": "0x23ae3c5b39b12f0693e05435eeaa1e51d8c61530", + "name": "Aigang Pre-Launch Token", + "symbol": "APT", + "decimals": 18 + }, + { + "address": "0xAc709FcB44a43c35F0DA4e3163b117A17F3770f5", + "name": "Arcade Token", + "symbol": "ARC", + "decimals": 18 + }, + { + "address": "0x1245ef80f4d9e02ed9425375e8f649b9221b31d8", + "name": "ArbitrageCT", + "symbol": "ARCT", + "decimals": 8 + }, + { + "address": "0x75aa7b0d02532f3833b66c7f0ad35376d373ddf8", + "name": "Accord", + "symbol": "ARD", + "decimals": 18 + }, + { + "address": "0xBA5F11b16B155792Cf3B2E6880E8706859A8AEB6", + "name": "Aeron", + "symbol": "ARN", + "decimals": 8 + }, + { + "address": "0xfec0cF7fE078a500abf15F1284958F22049c2C7e", + "name": "Maecenas", + "symbol": "ART", + "decimals": 18 + }, + { + "address": "0x7705FaA34B16EB6d77Dfc7812be2367ba6B0248e", + "name": "Artex Token", + "symbol": "ARX", + "decimals": 8 + }, + { + "address": "0x27054b13b1B798B345b591a4d22e6562d47eA75a", + "name": "AirSwap Token", + "symbol": "AST", + "decimals": 4 + }, + { + "address": "0x17052d51E954592C1046320c2371AbaB6C73Ef10", + "name": "ATHENIAN WARRIOR", + "symbol": "ATH", + "decimals": 18 + }, + { + "address": "0x78B7FADA55A64dD895D8c8c35779DD8b67fA8a05", + "name": "ATLANT Token", + "symbol": "ATL", + "decimals": 18 + }, + { + "address": "0x887834d3b8d450b6bab109c252df3da286d73ce4", + "name": "Atmatrix Token", + "symbol": "ATT", + "decimals": 18 + }, + { + "address": "0x0d88ed6e74bbfd96b831231638b66c05571e824f", + "name": "AVENTUS", + "symbol": "AVT", + "decimals": 18 + }, + { + "address": "0x0D8775F648430679A709E98d2b0Cb6250d2887EF", + "name": "Basic Attention Token", + "symbol": "BAT", + "decimals": 18 + }, + { + "address": "0x7367a68039d4704f30bfbf6d948020c3b07dfc59", + "name": "Beercoin", + "symbol": "\ud83c\udf7a", + "decimals": 18 + }, + { + "address": "0x1e797Ce986C3CFF4472F7D38d5C4aba55DfEFE40", + "name": "BCDN", + "symbol": "BCDN", + "decimals": 15 + }, + { + "address": "0xacfa209fb73bf3dd5bbfb1101b9bc999c49062a5", + "name": "Blockchain Certified Data Token", + "symbol": "BCDT", + "decimals": 18 + }, + { + "address": "0x1c4481750daa5Ff521A2a7490d9981eD46465Dbd", + "name": "BLOCKMASON CREDIT PROTOCOL TOKEN", + "symbol": "BCPT", + "decimals": 18 + }, + { + "address": "0x74C1E4b8caE59269ec1D85D3D4F324396048F4ac", + "name": "Beercoin", + "symbol": "ALE", + "decimals": 0 + }, + { + "address": "0x8aA33A7899FCC8eA5fBe6A608A109c3893A1B8b2", + "name": "Dao.Casino", + "symbol": "BET", + "decimals": 18 + }, + { + "address": "0xb2bfeb70b903f1baac7f2ba2c62934c7e5b974c4", + "name": "BetKing Bankroll Token", + "symbol": "BKB", + "decimals": 8 + }, + { + "address": "0x107c4504cd79C5d2696Ea0030a8dD4e92601B82e", + "name": "Bloom Token", + "symbol": "BLT", + "decimals": 18 + }, + { + "address": "0xce59d29b09aae565feeef8e52f47c3cd5368c663", + "name": "Bullioncoin", + "symbol": "BLX", + "decimals": 18 + }, + { + "address": "0xE5a7c12972f3bbFe70ed29521C8949b8Af6a0970", + "name": "Blockchain Index", + "symbol": "BLX", + "decimals": 18 + }, + { + "address": "0xdf6ef343350780bf8c3410bf062e0c015b1dd671", + "name": "Blackmoon Crypto Token", + "symbol": "BMC", + "decimals": 8 + }, + { + "address": "0xf028adee51533b1b47beaa890feb54a457f51e89", + "name": "BMChain Token", + "symbol": "BMT", + "decimals": 18 + }, + { + "address": "0x986EE2B944c42D017F52Af21c4c69B84DBeA35d8", + "name": "BitMartToken", + "symbol": "BMC", + "decimals": 18 + }, + { + "address": "0xb8c77482e45f1f44de1745f52c74426c631bdd52", + "name": "BNB", + "symbol": "BNB", + "decimals": 18 + }, + { + "address": "0xdD6Bf56CA2ada24c683FAC50E37783e55B57AF9F", + "name": "Brave New Coin", + "symbol": "BNC", + "decimals": 12 + }, + { + "address": "0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C", + "name": "Bancor Network Token", + "symbol": "BNT", + "decimals": 18 + }, + { + "address": "0xd2d6158683aee4cc838067727209a0aaf4359de3", + "name": "Bounty0x Token", + "symbol": "BNTY", + "decimals": 18 + }, + { + "address": "0xCc34366E3842cA1BD36c1f324d15257960fCC801", + "name": "Bonpay Token", + "symbol": "BON", + "decimals": 18 + }, + { + "address": "0x7f1e2c7d6a69bf34824d72c53b4550e895c0d8c2", + "name": "blockoptions", + "symbol": "BOP", + "decimals": 8 + }, + { + "address": "0xC2C63F23ec5E97efbD7565dF9Ec764FDc7d4e91d", + "name": "Boule Token", + "symbol": "BOU", + "decimals": 18 + }, + { + "address": "0x5Af2Be193a6ABCa9c8817001F45744777Db30756", + "name": "Bitquence", + "symbol": "BQX", + "decimals": 8 + }, + { + "address": "0x9E77D5a1251b6F7D456722A6eaC6D2d5980bd891", + "name": "BRAT RED", + "symbol": "BRAT", + "decimals": 8 + }, + { + "address": "0xf26ef5e0545384b7dcc0f297f2674189586830df", + "name": "BitsIdea", + "symbol": "BSDC", + "decimals": 18 + }, + { + "address": "0x0886949c1b8C412860c4264Ceb8083d1365e86CF", + "name": "EthereumBitcoin", + "symbol": "BTCE", + "decimals": 8 + }, + { + "address": "0x73dd069c299a5d691e9836243bcaec9c8c1d8734", + "name": "Bitcoineum", + "symbol": "BTE", + "decimals": 8 + }, + { + "address": "0x1961B3331969eD52770751fC718ef530838b6dEE", + "name": "BitDegree Token", + "symbol": "BDG", + "decimals": 18 + }, + { + "address": "0xfad572db566e5234ac9fc3d570c4edc0050eaa92", + "name": "Bytether", + "symbol": "BTH", + "decimals": 18 + }, + { + "address": "0x2accaB9cb7a48c3E82286F0b2f8798D201F4eC3f", + "name": "Battle", + "symbol": "BTL", + "decimals": 18 + }, + { + "address": "0x92685E93956537c25Bb75D5d47fca4266dd628B8", + "name": "Bitlle Token", + "symbol": "BTL", + "decimals": 4 + }, + { + "address": "0xcb97e65f07da24d46bcdd078ebebd7c6e6e3d750", + "name": "Bytom", + "symbol": "BTM", + "decimals": 8 + }, + { + "address": "0x16B0E62aC13a2fAeD36D18bce2356d25Ab3CfAD3", + "name": "Bitcoin Boutique", + "symbol": "BTQ", + "decimals": 18 + }, + { + "address": "0x26E75307Fc0C021472fEb8F727839531F112f317", + "name": "Crypto20", + "symbol": "C20", + "decimals": 18 + }, + { + "address": "0x7d4b8Cce0591C9044a22ee543533b72E976E36C3", + "name": "Change COIN", + "symbol": "CAG", + "decimals": 18 + }, + { + "address": "0x1d462414fe14cf489c7A21CaC78509f4bF8CD7c0", + "name": "CanYaCoin", + "symbol": "CAN", + "decimals": 6 + }, + { + "address": "0xe8780B48bdb05F928697A5e8155f672ED91462F7", + "name": "Cashaa", + "symbol": "CAS", + "decimals": 18 + }, + { + "address": "0x1234567461d3f8db7496581774bd869c83d51c93", + "name": "BitClave", + "symbol": "CAT", + "decimals": 18 + }, + { + "address": "0x68e14bb5A45B9681327E16E528084B9d962C1a39", + "name": "BitClave - Consumer Activity Token", + "symbol": "CAT", + "decimals": 18 + }, + { + "address": "0x56ba2Ee7890461f463F7be02aAC3099f6d5811A8", + "name": "BlockCAT Token", + "symbol": "CAT", + "decimals": 18 + }, + { + "address": "0xc166038705FFBAb3794185b3a9D925632A1DF37D", + "name": "Coal Coin", + "symbol": "CC3", + "decimals": 18 + }, + { + "address": "0x28577A6d31559bd265Ce3ADB62d0458550F7b8a7", + "name": "Crypto Crash Course", + "symbol": "CCC", + "decimals": 18 + }, + { + "address": "0xbe11eeb186e624b8f26a5045575a1340e4054552", + "name": "Crush Crypto Core", + "symbol": "CCC", + "decimals": 18 + }, + { + "address": "0xd348e07a2806505b856123045d27aeed90924b50", + "name": "Christ Coin", + "symbol": "CCLC", + "decimals": 8 + }, + { + "address": "0x8a95ca448A52C0ADf0054bB3402dC5e09CD6B232", + "name": "Confideal", + "symbol": "CDL", + "decimals": 18 + }, + { + "address": "0x177d39AC676ED1C67A2b268AD7F1E58826E5B0af", + "name": "CoinDash Token", + "symbol": "CDT", + "decimals": 18 + }, + { + "address": "0x6fFF3806Bbac52A20e0d79BC538d527f6a22c96b", + "name": "Commodity Ad Network", + "symbol": "CDX", + "decimals": 18 + }, + { + "address": "0x12FEF5e57bF45873Cd9B62E9DBd7BFb99e32D73e", + "name": "Cofoundit", + "symbol": "CFI", + "decimals": 18 + }, + { + "address": "0x06012c8cf97bead5deae237070f9587f8e7a266d", + "name": "CryptoKitties", + "symbol": "CK", + "decimals": 0 + }, + { + "address": "0x7fce2856899a6806eeef70807985fc7554c66340", + "name": "CLP Token", + "symbol": "CLP", + "decimals": 9 + }, + { + "address": "0x7e667525521cF61352e2E01b50FaaaE7Df39749a", + "name": "CMC", + "symbol": "CMC", + "decimals": 18 + }, + { + "address": "0xf85fEea2FdD81d51177F6b8F35F0e6734Ce45F5F", + "name": "CyberMiles Token", + "symbol": "CMT", + "decimals": 18 + }, + { + "address": "0xd4c435f5b09f855c3317c8524cb1f586e42795fa", + "name": "Cindicator Token", + "symbol": "CND", + "decimals": 18 + }, + { + "address": "0xB4b1D2C217EC0776584CE08D3DD98F90EDedA44b", + "name": "Climatecoin", + "symbol": "CO2", + "decimals": 18 + }, + { + "address": "0xb2f7eb1f2c37645be61d73953035360e768d81e6", + "name": "Cobinhood Token", + "symbol": "COB", + "decimals": 18 + }, + { + "address": "0x3136eF851592aCf49CA4C825131E364170FA32b3", + "name": "CoinFi", + "symbol": "COFI", + "decimals": 18 + }, + { + "address": "0x65292eeadf1426cd2df1c4793a3d7519f253913b", + "name": "COSS", + "symbol": "COSS", + "decimals": 18 + }, + { + "address": "0xAef38fBFBF932D1AeF3B808Bc8fBd8Cd8E1f8BC5", + "name": "CreditBIT", + "symbol": "CRB", + "decimals": 8 + }, + { + "address": "0x672a1AD4f667FB18A333Af13667aa0Af1F5b5bDD", + "name": "Verify Token", + "symbol": "CRED", + "decimals": 18 + }, + { + "address": "0x4e0603e2a27a30480e5e3a4fe548e29ef12f64be", + "name": "Credo Token", + "symbol": "CREDO", + "decimals": 18 + }, + { + "address": "0x80a7e048f37a50500351c204cb407766fa3bae7f", + "name": "CrypteriumToken", + "symbol": "CRPT", + "decimals": 18 + }, + { + "address": "0xE4c94d45f7Aef7018a5D66f44aF780ec6023378e", + "name": "CryptoCarbon", + "symbol": "", + "decimals": 0 + }, + { + "address": "0xbf4cfd7d1edeeea5f6600827411b41a21eb08abd", + "name": "CryptoLah", + "symbol": "CTL", + "decimals": 2 + }, + { + "address": "0xE3Fa177AcecfB86721Cf6f9f4206bd3Bd672D7d5", + "name": "ChainTrade Coin", + "symbol": "CTC", + "decimals": 18 + }, + { + "address": "0x662aBcAd0b7f345AB7FfB1b1fbb9Df7894f18e66", + "name": "CarTaxi", + "symbol": "CTX", + "decimals": 18 + }, + { + "address": "0xdA6cb58A0D0C01610a29c5A65c303e13e885887C", + "name": "cVToken", + "symbol": "cV", + "decimals": 18 + }, + { + "address": "0x41e5560054824ea6b0732e656e3ad64e20e94e45", + "name": "Civic", + "symbol": "CVC", + "decimals": 8 + }, + { + "address": "0xb6EE9668771a79be7967ee29a63D4184F8097143", + "name": "CargoX Token", + "symbol": "CXO", + "decimals": 18 + }, + { + "address": "0xdab0C31BF34C897Fb0Fe90D12EC9401caf5c36Ec", + "name": "DABcoin", + "symbol": "DAB", + "decimals": 0 + }, + { + "address": "0x07d9e49ea402194bf48a8276dafb16e4ed633317", + "name": "DALECOIN", + "symbol": "DALC", + "decimals": 8 + }, + { + "address": "0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413", + "name": "The DAO", + "symbol": "DAO", + "decimals": 16 + }, + { + "address": "0x81c9151de0c8bafcd325a57e3db5a5df1cebf79c", + "name": "DAT Token", + "symbol": "DAT", + "decimals": 18 + }, + { + "address": "0x1b5f21ee98eed48d292e8e2d3ed82b40a9728a22", + "name": "DataBroker DAO Token", + "symbol": "DATA", + "decimals": 18 + }, + { + "address": "0x0cf0ee63788a0849fe5297f3407f701e122cc023", + "name": "DATAcoin", + "symbol": "DATA", + "decimals": 18 + }, + { + "address": "0x399A0e6FbEb3d74c85357439f4c8AeD9678a5cbF", + "name": "DISLEDGER", + "symbol": "DCL", + "decimals": 3 + }, + { + "address": "0x08d32b0da63e2C3bcF8019c9c5d849d7a9d791e6", + "name": "Dentacoin", + "symbol": "\u0668", + "decimals": 0 + }, + { + "address": "0x08d32b0da63e2C3bcF8019c9c5d849d7a9d791e6", + "name": "Dentacoin", + "symbol": "\u0668", + "decimals": 0 + }, + { + "address": "0xcC4eF9EEAF656aC1a2Ab886743E98e97E090ed38", + "name": "Digital Developers Fund Token", + "symbol": "DDF", + "decimals": 18 + }, + { + "address": "0x3597bfd533a99c9aa083587b074434e61eb0a258", + "name": "DENT", + "symbol": "DENT", + "decimals": 8 + }, + { + "address": "0xE0B7927c4aF23765Cb51314A0E0521A9645F0E2A", + "name": "Digix DAO", + "symbol": "DGD", + "decimals": 9 + }, + { + "address": "0xf6cFe53d6FEbaEEA051f400ff5fc14F0cBBDacA1", + "name": "DigiPulse Token", + "symbol": "DGPT", + "decimals": 18 + }, + { + "address": "0x55b9a11c2e8351b4Ffc7b11561148bfaC9977855", + "name": "DigixGold", + "symbol": "DGX", + "decimals": 9 + }, + { + "address": "0x2e071D2966Aa7D8dECB1005885bA1977D6038A65", + "name": "DICE", + "symbol": "ROL", + "decimals": 16 + }, + { + "address": "0x13f11C9905A08ca76e3e853bE63D4f0944326C72", + "name": "Divi Exchange Token", + "symbol": "DIVX", + "decimals": 18 + }, + { + "address": "0x07e3c70653548b04f0a75970c1f81b4cbbfb606f", + "name": "Delta", + "symbol": "DLT", + "decimals": 18 + }, + { + "address": "0x2ccbFF3A042c68716Ed2a2Cb0c544A9f1d1935E1", + "name": "DMarket Token", + "symbol": "DMT", + "decimals": 8 + }, + { + "address": "0x0abdace70d3790235af448c88547603b945604ea", + "name": "district0x Network Token", + "symbol": "DNT", + "decimals": 18 + }, + { + "address": "0xE43E2041dc3786e166961eD9484a5539033d10fB", + "name": "DenCity", + "symbol": "DNX", + "decimals": 18 + }, + { + "address": "0xEEF6E90034eEa89E31Eb4B8eaCd323F28A92eaE4", + "name": "DOW", + "symbol": "dow", + "decimals": 18 + }, + { + "address": "0x01b3Ec4aAe1B8729529BEB4965F27d008788B0EB", + "name": "DA Power Play Token", + "symbol": "DPP", + "decimals": 18 + }, + { + "address": "0x419c4db4b9e25d6db2ad9691ccb832c8d9fda05e", + "name": "Dragon", + "symbol": "DRGN", + "decimals": 18 + }, + { + "address": "0x3c75226555FC496168d48B88DF83B95F16771F37", + "name": "Droplex Token", + "symbol": "DROP", + "decimals": 0 + }, + { + "address": "0x621d78f2ef2fd937bfca696cabaf9a779f59b3ed", + "name": "DCORP", + "symbol": "DRP", + "decimals": 2 + }, + { + "address": "0x1e09BD8Cadb441632e441Db3e1D79909EE0A2256", + "name": "Digital Safe Coin", + "symbol": "DSC", + "decimals": 1 + }, + { + "address": "0xd234bf2410a0009df9c3c63b610c09738f18ccd7", + "name": "Dynamic Trading Rights", + "symbol": "DTR", + "decimals": 8 + }, + { + "address": "0xd4cffeef10f60eca581b5e1146b5aca4194a4c3b", + "name": "Decentralized Universal Basic Income", + "symbol": "DUBI", + "decimals": 18 + }, + { + "address": "0x994f0dffdbae0bbf09b652d6f11a493fd33f42b9", + "name": "EagleCoin", + "symbol": "EAGLE", + "decimals": 18 + }, + { + "address": "0xafc39788c51f0c1ff7b55317f3e70299e521fff6", + "name": "eBitcoinCash", + "symbol": "eBCH", + "decimals": 8 + }, + { + "address": "0xeb7c20027172e5d143fb030d50f91cece2d1485d", + "name": "eBTC", + "symbol": "EBTC", + "decimals": 8 + }, + { + "address": "0xa578acc0cb7875781b7880903f4594d13cfa8b98", + "name": "EtherCarbon", + "symbol": "ECN", + "decimals": 2 + }, + { + "address": "0x17F93475d2A978f527c3f7c44aBf44AdfBa60D5C", + "name": "EtherCO2", + "symbol": "ECO2", + "decimals": 2 + }, + { + "address": "0x08711D3B02C8758F2FB3ab4e80228418a7F8e39c", + "name": "Edgeless", + "symbol": "EDG", + "decimals": 0 + }, + { + "address": "0xced4e93198734ddaff8492d525bd258d49eb388e", + "name": "Eidoo Token", + "symbol": "EDO", + "decimals": 18 + }, + { + "address": "0xb53a96bcbdd9cf78dff20bab6c2be7baec8f00f8", + "name": "ETHGAS", + "symbol": "eGAS", + "decimals": 8 + }, + { + "address": "0xf9F0FC7167c311Dd2F1e21E9204F87EBA9012fB2", + "name": "EasyHomes", + "symbol": "EHT", + "decimals": 8 + }, + { + "address": "0xc8C6A31A4A806d3710A7B38b7B296D2fABCCDBA8", + "name": "elixir", + "symbol": "ELIX", + "decimals": 18 + }, + { + "address": "0x44197a4c44d6a059297caf6be4f7e172bd56caaf", + "name": "ELTCOIN", + "symbol": "ELTCOIN", + "decimals": 8 + }, + { + "address": "0xb67b88a25708a35ae7c2d736d398d268ce4f7f83", + "name": "Etheremon", + "symbol": "EMON", + "decimals": 8 + }, + { + "address": "0xB802b24E0637c2B87D2E8b7784C055BBE921011a", + "name": "EthereumMovieVenture", + "symbol": "EMV", + "decimals": 2 + }, + { + "address": "0xF629cBd94d3791C9250152BD8dfBDF380E2a3B9c", + "name": "Enjin Coin", + "symbol": "ENJ", + "decimals": 18 + }, + { + "address": "0xd780Ae2Bf04cD96E577D3D014762f831d97129d0", + "name": "Envion", + "symbol": "EVN", + "decimals": 18 + }, + { + "address": "0x86fa049857e0209aa7d9e616f7eb3b3b78ecfdb0", + "name": "EOS", + "symbol": "EOS", + "decimals": 18 + }, + { + "address": "0xe8a1df958be379045e2b46a31a98b93a2ecdfded", + "name": "ESZCoin", + "symbol": "ESZ", + "decimals": 18 + }, + { + "address": "0x1b9743f556d65e757c4c650b4555baf354cb8bd3", + "name": "EthBits ETBS Token", + "symbol": "ETBS", + "decimals": 12 + }, + { + "address": "0x3a26746Ddb79B1B8e4450e3F4FFE3285A307387E", + "name": "EtherBIT", + "symbol": "ETHB", + "decimals": 8 + }, + { + "address": "0xabdf147870235fcfc34153828c769a70b3fae01f", + "name": "Tether EUR", + "symbol": "EURT", + "decimals": 6 + }, + { + "address": "0x923108a439C4e8C2315c4f6521E5cE95B44e9B4c", + "name": "Devery.io", + "symbol": "EVE", + "decimals": 18 + }, + { + "address": "0xf3db5fa2c66b7af3eb0c0b782510816cbe4813b8", + "name": "Everex", + "symbol": "EVX", + "decimals": 4 + }, + { + "address": "0xc98e0639c6d2ec037a615341c369666b110e80e5", + "name": "EXMR", + "symbol": "EXMR", + "decimals": 8 + }, + { + "address": "0x190e569bE071F40c704e15825F285481CB74B6cC", + "name": "Fame", + "symbol": "FAM", + "decimals": 12 + }, + { + "address": "0xf04a8ac553FceDB5BA99A64799155826C136b0Be", + "name": "Flixx", + "symbol": "FLIXX", + "decimals": 18 + }, + { + "address": "0x3a1Bda28AdB5B0a812a7CF10A1950c920F79BcD3", + "name": "FLIP Token", + "symbol": "FLP", + "decimals": 18 + }, + { + "address": "0x0ABeFb7611Cb3A01EA3FaD85f33C3C934F8e2cF4", + "name": "FARAD", + "symbol": "FRD", + "decimals": 18 + }, + { + "address": "0xe6f74dcfa0e20883008d8c16b6d9a329189d0c30", + "name": "FinTech Coin", + "symbol": "FTC", + "decimals": 2 + }, + { + "address": "0xab16e0d25c06cb376259cc18c1de4aca57605589", + "name": "FinallyUsableCryptoKarma", + "symbol": "FUCK", + "decimals": 4 + }, + { + "address": "0xEA38eAa3C86c8F9B751533Ba2E562deb9acDED40", + "name": "Fuel Token", + "symbol": "FUEL", + "decimals": 18 + }, + { + "address": "0x419D0d8BdD9aF5e606Ae2232ed285Aff190E711b", + "name": "FunFair", + "symbol": "FUN", + "decimals": 8 + }, + { + "address": "0x88FCFBc22C6d3dBaa25aF478C578978339BDe77a", + "name": "FundYourselfNow Token", + "symbol": "FYN", + "decimals": 18 + }, + { + "address": "0xf67451dc8421f0e0afeb52faa8101034ed081ed9", + "name": "Gambit", + "symbol": "GAM", + "decimals": 8 + }, + { + "address": "0x4F4f0Db4de903B88f2B1a2847971E231D54F8fd3", + "name": "Geens Platform Token", + "symbol": "GEE", + "decimals": 8 + }, + { + "address": "0x24083bb30072643c3bb90b44b7285860a755e687", + "name": "SGelderGER", + "symbol": "GELD", + "decimals": 18 + }, + { + "address": "0xaE4f56F072c34C0a65B3ae3E4DB797D831439D93", + "name": "Gimli Token", + "symbol": "GIM", + "decimals": 8 + }, + { + "address": "0xb3Bd49E28f8F832b8d1E246106991e546c323502", + "name": "Global Messaging Token", + "symbol": "GMT", + "decimals": 18 + }, + { + "address": "0x6810e776880C02933D47DB1b9fc05908e5386b96", + "name": "Gnosis Token", + "symbol": "GNO", + "decimals": 18 + }, + { + "address": "0xa74476443119A942dE498590Fe1f2454d7D4aC0d", + "name": "Golem Network Token", + "symbol": "GNT", + "decimals": 18 + }, + { + "address": "0xeAb43193CF0623073Ca89DB9B712796356FA7414", + "name": "GOLDX", + "symbol": "GOLDX", + "decimals": 18 + }, + { + "address": "0x8C65e992297d5f092A756dEf24F4781a280198Ff", + "name": "GazeCoin", + "symbol": "GZE", + "decimals": 18 + }, + { + "address": "0x12b19d3e2ccc14da04fae33e63652ce469b3f2fd", + "name": "GRID Token", + "symbol": "GRID", + "decimals": 12 + }, + { + "address": "0xB70835D7822eBB9426B56543E391846C107bd32C", + "name": "Game.com Token", + "symbol": "GTC", + "decimals": 18 + }, + { + "address": "0x025abad9e518516fdaafbdcdb9701b37fb7ef0fa", + "name": "GoldenTickets", + "symbol": "GTKT", + "decimals": 0 + }, + { + "address": "0xf7B098298f7C69Fc14610bf71d5e02c60792894C", + "name": "Guppy", + "symbol": "GUP", + "decimals": 3 + }, + { + "address": "0x103c3A209da59d3E7C4A89307e66521e081CFDF0", + "name": "Genesis Vision Token", + "symbol": "GVT", + "decimals": 18 + }, + { + "address": "0x58ca3065c0f24c7c96aee8d6056b5b5decf9c2f8", + "name": "GXC", + "symbol": "GXC", + "decimals": 10 + }, + { + "address": "0x22F0AF8D78851b72EE799e05F54A77001586B18A", + "name": "Genevieve VC", + "symbol": "GXVC", + "decimals": 10 + }, + { + "address": "0x84543f868ec1b1fac510d49d13c069f64cd2d5f9", + "name": "HEdpAY", + "symbol": "Hdp.\u0444", + "decimals": 18 + }, + { + "address": "0xffe8196bc259e8dedc544d935786aa4709ec3e64", + "name": "Hedge", + "symbol": "HDG", + "decimals": 18 + }, + { + "address": "0xe9ff07809ccff05dae74990e25831d0bc5cbe575", + "name": "HEDPAY", + "symbol": "Hdp.\u0444", + "decimals": 18 + }, + { + "address": "0xba2184520A1cC49a6159c57e61E1844E085615B6", + "name": "HelloGold Token", + "symbol": "HGT", + "decimals": 8 + }, + { + "address": "0xa9240fBCAC1F0b9A6aDfB04a53c8E3B0cC1D1444", + "name": "ethereumhigh", + "symbol": "HIG", + "decimals": 18 + }, + { + "address": "0x14F37B574242D366558dB61f3335289a5035c506", + "name": "HackerGold", + "symbol": "HKG", + "decimals": 3 + }, + { + "address": "0xcbCC0F036ED4788F63FC0fEE32873d6A7487b908", + "name": "Humaniq", + "symbol": "HMQ", + "decimals": 8 + }, + { + "address": "0x554C20B7c486beeE439277b4540A434566dC4C02", + "name": "Decision Token", + "symbol": "HST", + "decimals": 18 + }, + { + "address": "0xC0Eb85285d83217CD7c891702bcbC0FC401E2D9D", + "name": "Hive Project", + "symbol": "HVN", + "decimals": 8 + }, + { + "address": "0x5a84969bb663fb64F6d015DcF9F622Aedc796750", + "name": "IDICE", + "symbol": "ICE", + "decimals": 18 + }, + { + "address": "0x888666CA69E0f178DED6D75b5726Cee99A87D698", + "name": "ICONOMI", + "symbol": "ICN", + "decimals": 18 + }, + { + "address": "0xa33e729bf4fdeb868b534e1f20523463d9c46bee", + "name": "ICO", + "symbol": "\u00a2", + "decimals": 10 + }, + { + "address": "0x014B50466590340D41307Cc54DCee990c8D58aa8", + "name": "ICOS", + "symbol": "ICOS", + "decimals": 6 + }, + { + "address": "0xb5a5f22694352c15b00323844ad545abb2b11028", + "name": "ICON", + "symbol": "ICX", + "decimals": 18 + }, + { + "address": "0x814cafd4782d2e728170fda68257983f03321c58", + "name": "IDEA Token", + "symbol": "IDEA", + "decimals": 0 + }, + { + "address": "0x7654915a1b82d6d2d0afc37c52af556ea8983c7e", + "name": "Feed", + "symbol": "IFT", + "decimals": 18 + }, + { + "address": "0x16662f73df3e79e54c6c5938b4313f92c524c120", + "name": "Ibiscoin", + "symbol": "IIC", + "decimals": 18 + }, + { + "address": "0x88AE96845e157558ef59e9Ff90E766E22E480390", + "name": "Digital Zone of Immaterial Pictorial Sensibility", + "symbol": "IKB", + "decimals": 0 + }, + { + "address": "0xe3831c5A982B279A198456D577cfb90424cb6340", + "name": "Immune Coin", + "symbol": "IMC", + "decimals": 6 + }, + { + "address": "0x22E5F62D0FA19974749faa194e3d3eF6d89c08d7", + "name": "Immortal", + "symbol": "IMT", + "decimals": 0 + }, + { + "address": "0xf8e386EDa857484f5a12e4B5DAa9984E06E73705", + "name": "Indorse Token", + "symbol": "IND", + "decimals": 18 + }, + { + "address": "0x5b2e4a700dfbc560061e957edec8f6eeeb74a320", + "name": "INS Token", + "symbol": "INS", + "decimals": 10 + }, + { + "address": "0xa8006c4ca56f24d6836727d106349320db7fef82", + "name": "Internxt", + "symbol": "INXT", + "decimals": 8 + }, + { + "address": "0x64CdF819d3E75Ac8eC217B3496d7cE167Be42e80", + "name": "InsurePal token", + "symbol": "IPL", + "decimals": 18 + }, + { + "address": "0xc34b21f6f8e51cc965c2393b3ccfa3b82beb2403", + "name": "IoT", + "symbol": "IoT", + "decimals": 6 + }, + { + "address": "0x0aeF06DcCCC531e581f0440059E6FfCC206039EE", + "name": "Intelligent Trading Technologies", + "symbol": "ITT", + "decimals": 8 + }, + { + "address": "0xfca47962d45adfdfd1ab2d972315db4ce7ccf094", + "name": "InsureX", + "symbol": "IXT", + "decimals": 8 + }, + { + "address": "0x0Aaf561eFF5BD9c8F911616933F84166A17cfE0C", + "name": "Jbox", + "symbol": "JBX", + "decimals": 0 + }, + { + "address": "0x8727c112c712c4a03371ac87a74dd6ab104af768", + "name": "Jetcoin", + "symbol": "JET", + "decimals": 18 + }, + { + "address": "0x773450335eD4ec3DB45aF74f34F2c85348645D39", + "name": "JetCoins", + "symbol": "JET", + "decimals": 18 + }, + { + "address": "0x72D32ac1c5E66BfC5b08806271f8eEF915545164", + "name": "CryptoKEE", + "symbol": "KEE", + "decimals": 0 + }, + { + "address": "0x4CC19356f2D37338b9802aa8E8fc58B0373296E7", + "name": "SelfKey", + "symbol": "KEY", + "decimals": 18 + }, + { + "address": "0x27695E09149AdC738A978e9A678F99E4c39e9eb9", + "name": "KickCoin", + "symbol": "KICK", + "decimals": 8 + }, + { + "address": "0x818Fc6C2Ec5986bc6E2CBf00939d90556aB12ce5", + "name": "Kin", + "symbol": "KIN", + "decimals": 18 + }, + { + "address": "0xdd974D5C2e2928deA5F71b9825b8b646686BD200", + "name": "Kyber Network Crystal", + "symbol": "KNC", + "decimals": 18 + }, + { + "address": "0x9541FD8B9b5FA97381783783CeBF2F5fA793C262", + "name": "Kaizen", + "symbol": "KZN", + "decimals": 8 + }, + { + "address": "0x2eb86e8fc520e0f6bb5d9af08f924fe70558ab89", + "name": "Logarithm", + "symbol": "LGR", + "decimals": 8 + }, + { + "address": "0xff18dbc487b4c2e3222d115952babfda8ba52f5f", + "name": "PureLifeCoin", + "symbol": "LIFE", + "decimals": 18 + }, + { + "address": "0x514910771af9ca656af840dff83e8264ecf986ca", + "name": "ChainLink Token", + "symbol": "LINK", + "decimals": 18 + }, + { + "address": "0xe2e6d4be086c6938b53b22144855eef674281639", + "name": "Link Platform", + "symbol": "LNK", + "decimals": 18 + }, + { + "address": "0x24A77c1F17C547105E14813e517be06b0040aa76", + "name": "Live Stars Token", + "symbol": "LIVE", + "decimals": 18 + }, + { + "address": "0x63e634330A20150DbB61B15648bC73855d6CCF07", + "name": "Lancer Token", + "symbol": "LNC", + "decimals": 18 + }, + { + "address": "0x6beb418fc6e1958204ac8baddcf109b8e9694966", + "name": "Linker Coin", + "symbol": "LNC", + "decimals": 18 + }, + { + "address": "0x5e3346444010135322268a4630d2ed5f8d09446c", + "name": "LockChain", + "symbol": "LOC", + "decimals": 18 + }, + { + "address": "0x21ae23b882a340a22282162086bc98d3e2b73018", + "name": "LookRev", + "symbol": "LOK", + "decimals": 18 + }, + { + "address": "0xEF68e7C694F40c8202821eDF525dE3782458639f", + "name": "loopring", + "symbol": "LRC", + "decimals": 18 + }, + { + "address": "0xFB12e3CcA983B9f59D90912Fd17F8D745A8B2953", + "name": "LUCKY", + "symbol": "LUCK", + "decimals": 0 + }, + { + "address": "0xa89b5934863447f6e4fc53b315a93e873bda69a3", + "name": "LuminoCoin", + "symbol": "LUM", + "decimals": 18 + }, + { + "address": "0xfa05A73FfE78ef8f1a739473e462c54bae6567D9", + "name": "Lunyr Token", + "symbol": "LUN", + "decimals": 18 + }, + { + "address": "0x3f4b726668da46f5e0e75aa5d478acec9f38210f", + "name": "MostExclusive.com-ETH", + "symbol": "M-ETH", + "decimals": 18 + }, + { + "address": "0x0F5D2fB29fb7d3CFeE444a200298f468908cC942", + "name": "Decentraland", + "symbol": "MANA", + "decimals": 18 + }, + { + "address": "0x386467f1f3ddbe832448650418311a479eecfc57", + "name": "Embers", + "symbol": "MBRS", + "decimals": 0 + }, + { + "address": "0x93E682107d1E9defB0b5ee701C71707a4B2E46Bc", + "name": "MCAP", + "symbol": "MCAP", + "decimals": 8 + }, + { + "address": "0x138A8752093F4f9a79AaeDF48d4B9248fab93c9C", + "name": "Musiconomi", + "symbol": "MCI", + "decimals": 18 + }, + { + "address": "0xB63B606Ac810a52cCa15e44bB630fd42D8d1d83d", + "name": "Monaco", + "symbol": "MCO", + "decimals": 8 + }, + { + "address": "0x51DB5Ad35C671a87207d88fC11d593AC0C8415bd", + "name": "Moeda Loyalty Points", + "symbol": "MDA", + "decimals": 18 + }, + { + "address": "0x40395044ac3c0c57051906da938b54bd6557f212", + "name": "MobileGo Token", + "symbol": "MGO", + "decimals": 8 + }, + { + "address": "0xe23cd160761f63FC3a1cF78Aa034b6cdF97d3E0C", + "name": "Mainstreet Token", + "symbol": "MIT", + "decimals": 18 + }, + { + "address": "0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2", + "name": "Maker", + "symbol": "MKR", + "decimals": 18 + }, + { + "address": "0xc66ea802717bfb9833400264dd12c2bceaa34a6d", + "name": "MKR", + "symbol": "MKR", + "decimals": 18 + }, + { + "address": "0xBEB9eF514a379B997e0798FDcC901Ee474B6D9A1", + "name": "Melon Token", + "symbol": "MLN", + "decimals": 18 + }, + { + "address": "0x1a95B271B0535D15fa49932Daba31BA612b52946", + "name": "minereum", + "symbol": "MNE", + "decimals": 8 + }, + { + "address": "0xA9877b1e05D035899131DBd1e403825166D09f92", + "name": "Media Network Token", + "symbol": "MNT", + "decimals": 18 + }, + { + "address": "0x83cee9e086a77e492ee0bb93c2b0437ad6fdeccc", + "name": "Goldmint MNT Prelaunch Token", + "symbol": "MNTP", + "decimals": 18 + }, + { + "address": "0x957c30aB0426e0C93CD8241E2c60392d08c6aC8e", + "name": "Modum Token", + "symbol": "MOD", + "decimals": 0 + }, + { + "address": "0xAB6CF87a50F17d7F5E1FEaf81B6fE9FfBe8EBF84", + "name": "Macroverse Token", + "symbol": "MRV", + "decimals": 18 + }, + { + "address": "0x68AA3F232dA9bdC2343465545794ef3eEa5209BD", + "name": "Mothership Token", + "symbol": "MSP", + "decimals": 18 + }, + { + "address": "0xaF4DcE16Da2877f8c9e00544c93B62Ac40631F16", + "name": "Monetha", + "symbol": "MTH", + "decimals": 5 + }, + { + "address": "0xF433089366899D83a9f26A773D59ec7eCF30355e", + "name": "Metal", + "symbol": "MTL", + "decimals": 8 + }, + { + "address": "0x7FC408011165760eE31bE2BF20dAf450356692Af", + "name": "Mitrav", + "symbol": "MTR", + "decimals": 8 + }, + { + "address": "0x0AF44e2784637218dD1D32A322D44e603A8f0c6A", + "name": "MatryxToken", + "symbol": "MTX", + "decimals": 18 + }, + { + "address": "0x6425c6be902d692ae2db752b3c268afadb099d3b", + "name": "RED MWAT", + "symbol": "MWAT", + "decimals": 18 + }, + { + "address": "0xf7e983781609012307f2514f63D526D83D24F466", + "name": "MyEtherWallet Donations Token", + "symbol": "MYD", + "decimals": 16 + }, + { + "address": "0xa645264C5603E96c3b0B078cdab68733794B0A71", + "name": "Mysterium", + "symbol": "MYST", + "decimals": 8 + }, + { + "address": "0xa54ddc7b3cce7fc8b1e3fa0256d0db80d2c10970", + "name": "NEVERDIE", + "symbol": "NDC", + "decimals": 18 + }, + { + "address": "0xcfb98637bcae43C13323EAa1731cED2B716962fD", + "name": "Nimiq Exchange", + "symbol": "NET", + "decimals": 18 + }, + { + "address": "0xa823e6722006afe99e91c30ff5295052fe6b8e32", + "name": "Neumark", + "symbol": "NEU", + "decimals": 18 + }, + { + "address": "0xe26517A9967299453d3F1B48Aa005E6127e67210", + "name": "NIMFA Token", + "symbol": "NIMFA", + "decimals": 18 + }, + { + "address": "0x1776e1F26f98b1A5dF9cD347953a26dd3Cb46671", + "name": "Numeraire", + "symbol": "NMR", + "decimals": 18 + }, + { + "address": "0xec46f8207d766012454c408de210bcbc2243e71c", + "name": "Nitro", + "symbol": "NOX", + "decimals": 18 + }, + { + "address": "0xb91318f35bdb262e9423bc7c7c2a3a93dd93c92c", + "name": "Nuls", + "symbol": "NULS", + "decimals": 18 + }, + { + "address": "0x45e42D659D9f9466cD5DF622506033145a9b89Bc", + "name": "Nexium", + "symbol": "NxC", + "decimals": 3 + }, + { + "address": "0x7627de4b93263a6a7570b8dafa64bae812e5c394", + "name": "Nexxus", + "symbol": "NXX", + "decimals": 8 + }, + { + "address": "0x5c6183d10A00CD747a6Dbb5F658aD514383e9419", + "name": "Nexxus", + "symbol": "NXX", + "decimals": 8 + }, + { + "address": "0x701C244b988a513c945973dEFA05de933b23Fe1D", + "name": "openANX Token", + "symbol": "OAX", + "decimals": 18 + }, + { + "address": "0x7F2176cEB16dcb648dc924eff617c3dC2BEfd30d", + "name": "Ohni", + "symbol": "Ohni", + "decimals": 0 + }, + { + "address": "0xd26114cd6EE289AccF82350c8d8487fedB8A0C07", + "name": "OMGToken", + "symbol": "OMG", + "decimals": 18 + }, + { + "address": "0xd341d1680eeee3255b8c4c75bcce7eb57f144dae", + "name": "onG", + "symbol": "ONG", + "decimals": 18 + }, + { + "address": "0xb23be73573bc7e03db6e5dfc62405368716d28a8", + "name": "oneK", + "symbol": "ONEK", + "decimals": 18 + }, + { + "address": "0x4355fC160f74328f9b383dF2EC589bB3dFd82Ba0", + "name": "Opus Token", + "symbol": "OPT", + "decimals": 18 + }, + { + "address": "0x2C4e8f2D746113d0696cE89B35F0d8bF88E0AEcA", + "name": "Simple Token", + "symbol": "ST", + "decimals": 18 + }, + { + "address": "0x65a15014964f2102ff58647e16a16a6b9e14bcf6", + "name": "Ox Fina", + "symbol": "OX", + "decimals": 3 + }, + { + "address": "0x694404595e3075a942397f466aacd462ff1a7bd0", + "name": "smartillions.io Class 1 ETH", + "symbol": "PATENTS", + "decimals": 18 + }, + { + "address": "0xB97048628DB6B661D4C2aA833e95Dbe1A905B280", + "name": "TenX Pay Token", + "symbol": "PAY", + "decimals": 18 + }, + { + "address": "0x55648de19836338549130b1af587f16bea46f66b", + "name": "Pebbles", + "symbol": "PBL", + "decimals": 18 + }, + { + "address": "0x53148Bb4551707edF51a1e8d7A93698d18931225", + "name": "Peculium", + "symbol": "PCL", + "decimals": 8 + }, + { + "address": "0xec18f898b4076a3e18f1089d33376cc380bde61d", + "name": "Petro", + "symbol": "PETRO", + "decimals": 18 + }, + { + "address": "0x55c2A0C171D920843560594dE3d6EEcC09eFc098", + "name": "PEX-Token", + "symbol": "PEXT", + "decimals": 4 + }, + { + "address": "0xE64509F0bf07ce2d29A7eF19A8A9bc065477C1B4", + "name": "PiplCoin", + "symbol": "PIPL", + "decimals": 8 + }, + { + "address": "0x8eFFd494eB698cc399AF6231fCcd39E08fd20B15", + "name": "PIX Token", + "symbol": "PIX", + "decimals": 0 + }, + { + "address": "0xE477292f1B3268687A29376116B0ED27A9c76170", + "name": "Herocoin", + "symbol": "PLAY", + "decimals": 18 + }, + { + "address": "0x0AfFa06e7Fbe5bC9a764C979aA66E8256A631f02", + "name": "Polybius", + "symbol": "PLBT", + "decimals": 6 + }, + { + "address": "0xe3818504c1B32bF1557b16C238B2E01Fd3149C17", + "name": "PILLAR", + "symbol": "PLR", + "decimals": 18 + }, + { + "address": "0xD8912C10681D8B21Fd3742244f44658dBA12264E", + "name": "Pluton", + "symbol": "PLU", + "decimals": 18 + }, + { + "address": "0x0e0989b1f9b8a38983c2ba8053269ca62ec9b195", + "name": "Po.et", + "symbol": "POE", + "decimals": 8 + }, + { + "address": "0x43f6a1be992dee408721748490772b15143ce0a7", + "name": "Potatoin", + "symbol": "POIN", + "decimals": 0 + }, + { + "address": "0x779B7b713C86e3E6774f5040D9cCC2D43ad375F8", + "name": "StakePool", + "symbol": "POOL", + "decimals": 8 + }, + { + "address": "0xee609fe292128cad03b786dbb9bc2634ccdbe7fc", + "name": "PoSToken", + "symbol": "POS", + "decimals": 18 + }, + { + "address": "0x595832f8fc6bf59c85c527fec3740a1b7a361269", + "name": "PowerLedger", + "symbol": "POWR", + "decimals": 6 + }, + { + "address": "0xc42209accc14029c1012fb5680d95fbd6036e2a0", + "name": "PayPie", + "symbol": "PPP", + "decimals": 18 + }, + { + "address": "0xd4fa1460F537bb9085d22C7bcCB5DD450Ef28e3a", + "name": "Populous Platform", + "symbol": "PPT", + "decimals": 8 + }, + { + "address": "0x88a3e4f35d64aad41a6d4030ac9afe4356cb84fa", + "name": "Presearch", + "symbol": "PRE", + "decimals": 18 + }, + { + "address": "0x7728dfef5abd468669eb7f9b48a7f70a501ed29d", + "name": "ParagonCoin", + "symbol": "PRG", + "decimals": 6 + }, + { + "address": "0x7641b2Ca9DDD58adDf6e3381c1F994Aac5f1A32f", + "name": "Purpose", + "symbol": "PRPS", + "decimals": 18 + }, + { + "address": "0x1844b21593262668b7248d0f57a220caaba46ab9", + "name": "Oyster Pearl", + "symbol": "PRL", + "decimals": 18 + }, + { + "address": "0x226bb599a12C826476e3A771454697EA52E9E220", + "name": "Propy", + "symbol": "PRO", + "decimals": 8 + }, + { + "address": "0x163733bcc28dbf26B41a8CfA83e369b5B3af741b", + "name": "Persian", + "symbol": "PRS", + "decimals": 18 + }, + { + "address": "0x0c04d4f331da8df75f9e2e271e3f3f1494c66c36", + "name": "Prosper", + "symbol": "PRSP", + "decimals": 9 + }, + { + "address": "0x66497a283e0a007ba3974e837784c6ae323447de", + "name": "PornToken", + "symbol": "PT", + "decimals": 18 + }, + { + "address": "0x8Ae4BF2C33a8e667de34B54938B0ccD03Eb8CC06", + "name": "Patientory", + "symbol": "PTOY", + "decimals": 8 + }, + { + "address": "0x5512e1d6a7be424b4323126b4f9e86d023f95764", + "name": "PornTokenV2", + "symbol": "PTWO", + "decimals": 18 + }, + { + "address": "0xc14830e53aa344e8c14603a91229a0b925b0b262", + "name": "Populous XBRL token", + "symbol": "PXT", + "decimals": 8 + }, + { + "address": "0x671AbBe5CE652491985342e85428EB1b07bC6c64", + "name": "Quantum", + "symbol": "QAU", + "decimals": 8 + }, + { + "address": "0x697beac28B09E122C4332D163985e8a73121b97F", + "name": "QRL", + "symbol": "QRL", + "decimals": 8 + }, + { + "address": "0x99ea4dB9EE77ACD40B119BD1dC4E33e1C070b80d", + "name": "Quantstamp Token", + "symbol": "QSP", + "decimals": 18 + }, + { + "address": "0x2C3C1F05187dBa7A5f2Dd47Dca57281C4d4F183F", + "name": "Q", + "symbol": "QTQ", + "decimals": 18 + }, + { + "address": "0x9a642d6b3368ddc662CA244bAdf32cDA716005BC", + "name": "Qtum", + "symbol": "QTUM", + "decimals": 18 + }, + { + "address": "0x255aa6df07540cb5d3d297f0d0d4d84cb52bc8e6", + "name": "Raiden Token", + "symbol": "RDN", + "decimals": 18 + }, + { + "address": "0x5f53f7a8075614b699baad0bc2c899f4bad8fbbf", + "name": "Rebellious", + "symbol": "REBL", + "decimals": 18 + }, + { + "address": "0xE94327D07Fc17907b4DB788E5aDf2ed424adDff6", + "name": "Reputation", + "symbol": "REP", + "decimals": 18 + }, + { + "address": "0x8f8221aFbB33998d8584A2B05749bA73c37a938a", + "name": "Request Token", + "symbol": "REQ", + "decimals": 18 + }, + { + "address": "0xf05a9382A4C3F29E2784502754293D88b835109C", + "name": "REX - Real Estate tokens", + "symbol": "REX", + "decimals": 18 + }, + { + "address": "0xdd007278b667f6bef52fd0a4c23604aa1f96039a", + "name": "RiptideCoin", + "symbol": "RIPT", + "decimals": 8 + }, + { + "address": "0x607F4C5BB672230e8672085532f7e901544a7375", + "name": "iEx.ec Network Token", + "symbol": "RLC", + "decimals": 9 + }, + { + "address": "0xcCeD5B8288086BE8c38E23567e684C3740be4D48", + "name": "Roulette Token", + "symbol": "RLT", + "decimals": 10 + }, + { + "address": "0x4a42d2c580f83dce404acad18dab26db11a1750e", + "name": "Relex", + "symbol": "RLX", + "decimals": 18 + }, + { + "address": "0x0996bfb5d057faa237640e2506be7b4f9c46de0b", + "name": "Render Token", + "symbol": "RNDR", + "decimals": 18 + }, + { + "address": "0xc9de4b7f0c3d991e967158e4d4bfa4b51ec0b114", + "name": "ROK Token", + "symbol": "ROK", + "decimals": 18 + }, + { + "address": "0x4993CB95c7443bdC06155c5f5688Be9D8f6999a5", + "name": "ROUND", + "symbol": "ROUND", + "decimals": 18 + }, + { + "address": "0xb4efd85c19999d84251304bda99e90b92300bd93", + "name": "Rocket Pool", + "symbol": "RPL", + "decimals": 18 + }, + { + "address": "0x54b293226000ccBFC04DF902eEC567CB4C35a903", + "name": "RiderToken", + "symbol": "RTN", + "decimals": 18 + }, + { + "address": "0x3d1ba9be9f66b8ee101911bc36d3fb562eac2244", + "name": "RvT", + "symbol": "RVT", + "decimals": 18 + }, + { + "address": "0x1ec8fe51a9b6a3a6c427d17d9ecc3060fbc4a45c", + "name": "smartillions.io A ETH", + "symbol": "S-A-PAT", + "decimals": 18 + }, + { + "address": "0x3eb91d237e491e0dee8582c402d85cb440fb6b54", + "name": "Smartillions.ch-ETH", + "symbol": "S-ETH", + "decimals": 18 + }, + { + "address": "0x4156D3342D5c385a87D264F90653733592000581", + "name": "Salt", + "symbol": "SALT", + "decimals": 8 + }, + { + "address": "0x7C5A0CE9267ED19B22F8cae653F198e3E8daf098", + "name": "SANtiment network token", + "symbol": "SAN", + "decimals": 18 + }, + { + "address": "0xd7631787b4dcc87b1254cfd1e5ce48e96823dee8", + "name": "SOCIAL", + "symbol": "SCL", + "decimals": 8 + }, + { + "address": "0x6745fAB6801e376cD24F03572B9C9B0D4EdDDCcf", + "name": "Sense", + "symbol": "SENSE", + "decimals": 8 + }, + { + "address": "0x4ca74185532dc1789527194e5b9c866dd33f4e82", + "name": "sensatori", + "symbol": "sense", + "decimals": 18 + }, + { + "address": "0xe06eda7435ba749b047380ced49121dde93334ae", + "name": "Transferable Sydney Ethereum Token", + "symbol": "SET", + "decimals": 0 + }, + { + "address": "0x98f5e9b7f0e33956c0443e81bf7deb8b5b1ed545", + "name": "Sexy Token", + "symbol": "SEXY", + "decimals": 18 + }, + { + "address": "0xa1ccc166faf0E998b3E33225A1A0301B1C86119D", + "name": "SGELDER", + "symbol": "SGEL", + "decimals": 18 + }, + { + "address": "0xd248B0D48E44aaF9c49aea0312be7E13a6dc1468", + "name": "Status Genesis Token", + "symbol": "SGT", + "decimals": 1 + }, + { + "address": "0xEF2E9966eb61BB494E5375d5Df8d67B7dB8A780D", + "name": "Shitcoin", + "symbol": "SHIT", + "decimals": 0 + }, + { + "address": "0x8a187d5285d316bcbc9adafc08b51d70a0d8e000", + "name": "Smart Investment Fund Token", + "symbol": "SIFT", + "decimals": 0 + }, + { + "address": "0x2bDC0D42996017fCe214b21607a515DA41A9E0C5", + "name": "SkinCoin", + "symbol": "SKIN", + "decimals": 6 + }, + { + "address": "0x4994e81897a920c0FEA235eb8CEdEEd3c6fFF697", + "name": "Sikoba Continuous Sale", + "symbol": "SKO1", + "decimals": 18 + }, + { + "address": "0x4c382F8E09615AC86E08CE58266CC227e7d4D913", + "name": "Skrilla", + "symbol": "SKR", + "decimals": 6 + }, + { + "address": "0x6E34d8d84764D40f6D7b39cd569Fd017bF53177D", + "name": "Skraps", + "symbol": "SKRP", + "decimals": 18 + }, + { + "address": "0x7A5fF295Dc8239d5C2374E4D894202aAF029Cab6", + "name": "Smartlands Token", + "symbol": "SLT", + "decimals": 3 + }, + { + "address": "0x6F6DEb5db0C4994A8283A01D6CFeEB27Fc3bBe9C", + "name": "SmartBillions Token", + "symbol": "Smart", + "decimals": 0 + }, + { + "address": "0xF4134146AF2d511Dd5EA8cDB1C4AC88C57D60404", + "name": "SunContract", + "symbol": "SNC", + "decimals": 18 + }, + { + "address": "0x44F588aEeB8C44471439D1270B3603c66a9262F1", + "name": "SnipCoin", + "symbol": "SNIP", + "decimals": 18 + }, + { + "address": "0xf333b2Ace992ac2bBD8798bF57Bc65a06184afBa", + "name": "SND Token 1.0", + "symbol": "SND", + "decimals": 0 + }, + { + "address": "0xaeC2E87E0A235266D9C5ADc9DEb4b2E29b54D009", + "name": "SingularDTV", + "symbol": "SNGLS", + "decimals": 0 + }, + { + "address": "0x983F6d60db79ea8cA4eB9968C6aFf8cfA04B3c63", + "name": "SONM Token", + "symbol": "SNM", + "decimals": 18 + }, + { + "address": "0x744d70FDBE2Ba4CF95131626614a1763DF805B9E", + "name": "Status Network", + "symbol": "SNT", + "decimals": 18 + }, + { + "address": "0xbdc5bac39dbe132b1e030e898ae3830017d7d969", + "name": "Snovio", + "symbol": "SNOV", + "decimals": 18 + }, + { + "address": "0x1f54638b7737193ffd86c19ec51907a7c41755d8", + "name": "Sola Token", + "symbol": "SOL", + "decimals": 6 + }, + { + "address": "0x42d6622dece394b54999fbd73d108123806f6a18", + "name": "SPANK", + "symbol": "SPANK", + "decimals": 18 + }, + { + "address": "0x58bf7df57d9DA7113c4cCb49d8463D4908C735cb", + "name": "Science Power and Research Coin", + "symbol": "SPARC", + "decimals": 18 + }, + { + "address": "0x24aef3bf1a47561500f9430d74ed4097c47f51f2", + "name": "SPARTA", + "symbol": "SPARTA", + "decimals": 4 + }, + { + "address": "0x85089389C14Bd9c77FC2b8F0c3d1dC3363Bf06Ef", + "name": "SPFToken", + "symbol": "SPF", + "decimals": 18 + }, + { + "address": "0x68d57c9a1C35f63E2c83eE8e49A64e9d70528D25", + "name": "SIRIN", + "symbol": "SRN", + "decimals": 18 + }, + { + "address": "0x9a005c9a89bd72a4bd27721e7a09a3c11d2b03c4", + "name": "StarterCoin", + "symbol": "STAC", + "decimals": 18 + }, + { + "address": "0xF70a642bD387F94380fFb90451C2c81d4Eb82CBc", + "name": "Starbase", + "symbol": "STAR", + "decimals": 18 + }, + { + "address": "0x629aEe55ed49581C33ab27f9403F7992A289ffd5", + "name": "StrikeCoin Token", + "symbol": "STC", + "decimals": 18 + }, + { + "address": "0x7dd7f56d697cc0f2b52bd55c057f378f1fe6ab4b", + "name": "$TEAK", + "symbol": "$TEAK", + "decimals": 18 + }, + { + "address": "0x599346779e90fc3F5F997b5ea715349820F91571", + "name": "Saturn", + "symbol": "STN", + "decimals": 4 + }, + { + "address": "0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC", + "name": "StorjToken", + "symbol": "STORJ", + "decimals": 8 + }, + { + "address": "0xD0a4b8946Cb52f0661273bfbC6fD0E0C75Fc6433", + "name": "Storm Token", + "symbol": "STORM", + "decimals": 18 + }, + { + "address": "0xecd570bBf74761b960Fa04Cc10fe2c4e86FfDA36", + "name": "STASHPAY", + "symbol": "STP", + "decimals": 8 + }, + { + "address": "0x46492473755e8dF960F8034877F61732D718CE96", + "name": "StarCredits", + "symbol": "STRC", + "decimals": 8 + }, + { + "address": "0x006BeA43Baa3f7A6f765F14f10A1a1b08334EF45", + "name": "Stox", + "symbol": "STX", + "decimals": 18 + }, + { + "address": "0x12480E24eb5bec1a9D4369CaB6a80caD3c0A377A", + "name": "Substratum", + "symbol": "SUB", + "decimals": 2 + }, + { + "address": "0x9e88613418cf03dca54d6a2cf6ad934a78c7a17a", + "name": "Swarm Fund Token", + "symbol": "SWM", + "decimals": 18 + }, + { + "address": "0xB9e7F8568e08d5659f5D29C4997173d84CdF2607", + "name": "Swarm City Token", + "symbol": "SWT", + "decimals": 18 + }, + { + "address": "0x12b306fa98f4cbb8d4457fdff3a0a0a56f07ccdf", + "name": "Spectre.ai D-Token", + "symbol": "SXDT", + "decimals": 18 + }, + { + "address": "0x2c82c73d5b34aa015989462b2948cd616a37641f", + "name": "Spectre.ai U-Token", + "symbol": "SXUT", + "decimals": 18 + }, + { + "address": "0x10b123fddde003243199aad03522065dc05827a0", + "name": "Synapse", + "symbol": "SYN", + "decimals": 18 + }, + { + "address": "0xE7775A6e9Bcf904eb39DA2b68c5efb4F9360e08C", + "name": "Token-as-a-Service", + "symbol": "TAAS", + "decimals": 6 + }, + { + "address": "0xc27a2f05fa577a83ba0fdb4c38443c0718356501", + "name": "Lamden Tau", + "symbol": "TAU", + "decimals": 18 + }, + { + "address": "0xFACCD5Fc83c3E4C3c1AC1EF35D15adf06bCF209C", + "name": "TheBillionCoin2", + "symbol": "TBC2", + "decimals": 8 + }, + { + "address": "0xAFe60511341a37488de25Bef351952562E31fCc1", + "name": "TBOT", + "symbol": "TBT", + "decimals": 8 + }, + { + "address": "0x85e076361cc813a908ff672f9bad1541474402b2", + "name": "Telcoin", + "symbol": "TEL", + "decimals": 2 + }, + { + "address": "0xa7f976C360ebBeD4465c2855684D1AAE5271eFa9", + "name": "TrueFlip", + "symbol": "TFL", + "decimals": 8 + }, + { + "address": "0x6531f133e6DeeBe7F2dcE5A0441aA7ef330B4e53", + "name": "Chronobank TIME", + "symbol": "TIME", + "decimals": 8 + }, + { + "address": "0x80bc5512561c7f85a3a9508c7df7901b370fa1df", + "name": "TradeToken", + "symbol": "TIO", + "decimals": 18 + }, + { + "address": "0xEa1f346faF023F974Eb5adaf088BbCdf02d761F4", + "name": "Blocktix", + "symbol": "TIX", + "decimals": 18 + }, + { + "address": "0xaAAf91D9b90dF800Df4F55c205fd6989c977E73a", + "name": "Monolith TKN", + "symbol": "TKN", + "decimals": 8 + }, + { + "address": "0x08f5a9235b08173b7569f83645d2c7fb55e8ccd8", + "name": "Tierion Network Token", + "symbol": "TNT", + "decimals": 8 + }, + { + "address": "0xcb94be6f13a1182e4a4b6140cb7bf2025d28e41b", + "name": "Trustcoin", + "symbol": "TRST", + "decimals": 6 + }, + { + "address": "0xf230b790e05390fc8295f4d3f60332c93bed42e2", + "name": "Tronix", + "symbol": "TRX", + "decimals": 6 + }, + { + "address": "0x2eF1aB8a26187C58BB8aAeB11B2fC6D25C5c0716", + "name": "TWN Shares", + "symbol": "TWN", + "decimals": 18 + }, + { + "address": "0x24692791bc444c5cd0b81e3cbcaba4b04acd1f3b", + "name": "UnikoinGold", + "symbol": "UKG", + "decimals": 18 + }, + { + "address": "0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7", + "name": "Unicorns", + "symbol": "\ud83e\udd84", + "decimals": 0 + }, + { + "address": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "name": "Tether USD", + "symbol": "USDT", + "decimals": 6 + }, + { + "address": "0xd01db73e047855efb414e6202098c4be4cd2423b", + "name": "Uquid Coin", + "symbol": "UQC", + "decimals": 18 + }, + { + "address": "0x70a72833d6bf7f508c8224ce59ea1ef3d0ea3a38", + "name": "UTRUST Token", + "symbol": "UTK", + "decimals": 18 + }, + { + "address": "0x340d2bde5eb28c1eed91b2f790723e3b160613b7", + "name": "BLOCKv Token", + "symbol": "VEE", + "decimals": 18 + }, + { + "address": "0xEbeD4fF9fe34413db8fC8294556BBD1528a4DAca", + "name": "VENUS", + "symbol": "VENUS", + "decimals": 3 + }, + { + "address": "0x8f3470A7388c05eE4e7AF3d01D8C722b0FF52374", + "name": "Veritaseum", + "symbol": "VERI", + "decimals": 18 + }, + { + "address": "0xD850942eF8811f2A866692A623011bDE52a462C1", + "name": "VeChain Token", + "symbol": "VEN", + "decimals": 18 + }, + { + "address": "0xe8ff5c9c75deb346acac493c463c8950be03dfba", + "name": "Vibe Coin", + "symbol": "VIBE", + "decimals": 18 + }, + { + "address": "0x2C974B2d0BA1716E644c1FC59982a89DDD2fF724", + "name": "VIB", + "symbol": "VIB", + "decimals": 18 + }, + { + "address": "0x882448f83d90b2bf477af2ea79327fdea1335d93", + "name": "VIBEX Exchange Token", + "symbol": "VIBEX", + "decimals": 18 + }, + { + "address": "0x519475b31653e46d20cd09f9fdcf3b12bdacb4f5", + "name": "VIU", + "symbol": "VIU", + "decimals": 18 + }, + { + "address": "0x83eea00d838f92dec4d1475697b9f4d3537b56e3", + "name": "VOISE", + "symbol": "VOISE", + "decimals": 8 + }, + { + "address": "0xeDBaF3c5100302dCddA53269322f3730b1F0416d", + "name": "VEROS", + "symbol": "VRS", + "decimals": 5 + }, + { + "address": "0x5c543e7AE0A1104f78406C340E9C64FD9fCE5170", + "name": "vSlice", + "symbol": "VSL", + "decimals": 18 + }, + { + "address": "0x286BDA1413a2Df81731D4930ce2F862a35A609fE", + "name": "WaBi", + "symbol": "WaBi", + "decimals": 18 + }, + { + "address": "0x39Bb259F66E1C59d5ABEF88375979b4D20D98022", + "name": "Wax Token", + "symbol": "WAX", + "decimals": 8 + }, + { + "address": "0x74951B677de32D596EE851A233336926e6A2cd09", + "name": "We Bet Crypto", + "symbol": "WBA", + "decimals": 7 + }, + { + "address": "0x6a0a97e47d15aad1d132a1ac79a480e3f2079063", + "name": "WePower Contribution Token", + "symbol": "WCT", + "decimals": 18 + }, + { + "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "name": "Wrapped Ether", + "symbol": "WETH", + "decimals": 18 + }, + { + "address": "0x5e4ABE6419650CA839Ce5BB7Db422b881a6064bB", + "name": "Wi Coin", + "symbol": "WiC", + "decimals": 18 + }, + { + "address": "0x667088b212ce3d06a1b553a7221E1fD19000d9aF", + "name": "WINGS", + "symbol": "WINGS", + "decimals": 18 + }, + { + "address": "0xF6B55acBBC49f4524Aa48D19281A9A77c54DE10f", + "name": "WOLK TOKEN", + "symbol": "WLK", + "decimals": 18 + }, + { + "address": "0x728781E75735dc0962Df3a51d7Ef47E798A7107E", + "name": "Token Wolk Protocol Token", + "symbol": "WOLK", + "decimals": 18 + }, + { + "address": "0x62087245087125d3db5b9a3d713d78e7bbc31e54", + "name": "WorldPeaceCoin", + "symbol": "WPC", + "decimals": 18 + }, + { + "address": "0x910Dfc18D6EA3D6a7124A6F8B5458F281060fa4c", + "name": "X8XToken", + "symbol": "X8X", + "decimals": 18 + }, + { + "address": "0x4DF812F6064def1e5e029f1ca858777CC98D2D81", + "name": "Xaurum", + "symbol": "XAUR", + "decimals": 8 + }, + { + "address": "0x4d829f8c92a6691c56300d020c9e0db984cfe2ba", + "name": "CoinCrowd", + "symbol": "XCC", + "decimals": 18 + }, + { + "address": "0x533ef0984b2FAA227AcC620C67cce12aA39CD8CD", + "name": "XaurumGamma", + "symbol": "XGM", + "decimals": 8 + }, + { + "address": "0x30f4A3e0aB7a76733D8b60b89DD93c3D0b4c9E2f", + "name": "CryptogeneToken", + "symbol": "XGT", + "decimals": 18 + }, + { + "address": "0xB110eC7B1dcb8FAB8dEDbf28f53Bc63eA5BEdd84", + "name": "Sphre AIR", + "symbol": "XID", + "decimals": 8 + }, + { + "address": "0xab95e915c123fded5bdfb6325e35ef5515f1ea69", + "name": "XENON", + "symbol": "XNN", + "decimals": 18 + }, + { + "address": "0x572e6f318056ba0c5d47a422653113843d250691", + "name": "EXANTE Token", + "symbol": "XNT", + "decimals": 0 + }, + { + "address": "0xB24754bE79281553dc1adC160ddF5Cd9b74361a4", + "name": "RIALTO", + "symbol": "XRL", + "decimals": 9 + }, + { + "address": "0x0F513fFb4926ff82D7F60A05069047AcA295C413", + "name": "CrowdstartCoin", + "symbol": "XSC", + "decimals": 18 + }, + { + "address": "0x0F33bb20a282A7649C7B3AFf644F084a9348e933", + "name": "YUPIE", + "symbol": "YUP", + "decimals": 18 + }, + { + "address": "0x6781a0f84c7e9e846dcb84a9a5bd49333067b104", + "name": "ZAP TOKEN", + "symbol": "ZAP", + "decimals": 18 + }, + { + "address": "0x05f4a42e251f2d52b8ed15E9FEdAacFcEF1FAD27", + "name": "Zilliqa", + "symbol": "ZIL", + "decimals": 12 + }, + { + "address": "0xE41d2489571d322189246DaFA5ebDe1F4699F498", + "name": "0x Protocol Token", + "symbol": "ZRX", + "decimals": 18 + }, + { + "address": "0xe386b139ed3715ca4b18fd52671bdcea1cdfe4b1", + "name": "Zeus Token", + "symbol": "ZST", + "decimals": 8 + } +] \ No newline at end of file diff --git a/src/fonts/glyphicons.eot b/src/fonts/glyphicons.eot new file mode 100755 index 00000000..56792f69 Binary files /dev/null and b/src/fonts/glyphicons.eot differ diff --git a/src/fonts/glyphicons.svg b/src/fonts/glyphicons.svg new file mode 100755 index 00000000..48cddc5a --- /dev/null +++ b/src/fonts/glyphicons.svg @@ -0,0 +1,21 @@ + + + +Generated by IcoMoon + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/fonts/glyphicons.ttf b/src/fonts/glyphicons.ttf new file mode 100755 index 00000000..f1c30440 Binary files /dev/null and b/src/fonts/glyphicons.ttf differ diff --git a/src/fonts/glyphicons.woff b/src/fonts/glyphicons.woff new file mode 100755 index 00000000..a0fa4cb6 Binary files /dev/null and b/src/fonts/glyphicons.woff differ diff --git a/src/fonts/icomoon.eot b/src/fonts/icomoon.eot new file mode 100755 index 00000000..153870a4 Binary files /dev/null and b/src/fonts/icomoon.eot differ diff --git a/src/fonts/icomoon.svg b/src/fonts/icomoon.svg new file mode 100755 index 00000000..05e7fc9c --- /dev/null +++ b/src/fonts/icomoon.svg @@ -0,0 +1,41 @@ + + + +Generated by IcoMoon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/fonts/icomoon.ttf b/src/fonts/icomoon.ttf new file mode 100755 index 00000000..1e3aa3a8 Binary files /dev/null and b/src/fonts/icomoon.ttf differ diff --git a/src/fonts/icomoon.woff b/src/fonts/icomoon.woff new file mode 100755 index 00000000..65c1cc49 Binary files /dev/null and b/src/fonts/icomoon.woff differ diff --git a/src/fonts/pass.ttf b/src/fonts/pass.ttf new file mode 100644 index 00000000..c0f50b1b Binary files /dev/null and b/src/fonts/pass.ttf differ diff --git a/src/fonts/roboto/RobotoZero.eot b/src/fonts/roboto/RobotoZero.eot new file mode 100644 index 00000000..55531629 Binary files /dev/null and b/src/fonts/roboto/RobotoZero.eot differ diff --git a/src/fonts/roboto/RobotoZero.ttf b/src/fonts/roboto/RobotoZero.ttf new file mode 100644 index 00000000..524e29e0 Binary files /dev/null and b/src/fonts/roboto/RobotoZero.ttf differ diff --git a/src/fonts/roboto/RobotoZero.woff b/src/fonts/roboto/RobotoZero.woff new file mode 100755 index 00000000..2807d896 Binary files /dev/null and b/src/fonts/roboto/RobotoZero.woff differ diff --git a/src/images/bch-logo.png b/src/images/bch-logo.png new file mode 100644 index 00000000..9590c12e Binary files /dev/null and b/src/images/bch-logo.png differ diff --git a/src/images/btc-logo.png b/src/images/btc-logo.png new file mode 100644 index 00000000..5b77226c Binary files /dev/null and b/src/images/btc-logo.png differ diff --git a/src/images/btg-logo.png b/src/images/btg-logo.png new file mode 100644 index 00000000..5eda4570 Binary files /dev/null and b/src/images/btg-logo.png differ diff --git a/src/images/bth-logo.png b/src/images/bth-logo.png new file mode 100644 index 00000000..dcc596e2 Binary files /dev/null and b/src/images/bth-logo.png differ diff --git a/src/images/case.png b/src/images/case.png new file mode 100644 index 00000000..eee16f1d Binary files /dev/null and b/src/images/case.png differ diff --git a/src/images/dash-logo.png b/src/images/dash-logo.png new file mode 100644 index 00000000..5c3a113d Binary files /dev/null and b/src/images/dash-logo.png differ diff --git a/src/images/dashboard.png b/src/images/dashboard.png new file mode 100644 index 00000000..790afebf Binary files /dev/null and b/src/images/dashboard.png differ diff --git a/src/images/etc-logo.png b/src/images/etc-logo.png new file mode 100644 index 00000000..e868d6ea Binary files /dev/null and b/src/images/etc-logo.png differ diff --git a/src/images/eth-logo.png b/src/images/eth-logo.png new file mode 100644 index 00000000..5b7c1a8a Binary files /dev/null and b/src/images/eth-logo.png differ diff --git a/src/images/icons-spritesheet.svg b/src/images/icons-spritesheet.svg new file mode 100644 index 00000000..337b1466 --- /dev/null +++ b/src/images/icons-spritesheet.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/images/icontrezor.png b/src/images/icontrezor.png new file mode 100644 index 00000000..8c35659b Binary files /dev/null and b/src/images/icontrezor.png differ diff --git a/src/images/landingpage.png b/src/images/landingpage.png new file mode 100644 index 00000000..43e64752 Binary files /dev/null and b/src/images/landingpage.png differ diff --git a/src/images/ltc-logo.png b/src/images/ltc-logo.png new file mode 100644 index 00000000..201f2350 Binary files /dev/null and b/src/images/ltc-logo.png differ diff --git a/src/images/nem-logo.png b/src/images/nem-logo.png new file mode 100644 index 00000000..1b4cccf2 Binary files /dev/null and b/src/images/nem-logo.png differ diff --git a/src/images/satoshilabs.png b/src/images/satoshilabs.png new file mode 100644 index 00000000..4e3bc6e6 Binary files /dev/null and b/src/images/satoshilabs.png differ diff --git a/src/images/trezor-logo.svg b/src/images/trezor-logo.svg new file mode 100644 index 00000000..c44f5b6b --- /dev/null +++ b/src/images/trezor-logo.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + diff --git a/src/images/zec-logo.png b/src/images/zec-logo.png new file mode 100644 index 00000000..134ba792 Binary files /dev/null and b/src/images/zec-logo.png differ diff --git a/src/index.html b/src/index.html index eee65fbc..257d119f 100644 --- a/src/index.html +++ b/src/index.html @@ -4,7 +4,7 @@ - TrezorConnect boilerplate with React | TREZOR + Ethereum Wallet| TREZOR diff --git a/src/js/actions/AccountActions.js b/src/js/actions/AccountActions.js new file mode 100644 index 00000000..8f2b82f3 --- /dev/null +++ b/src/js/actions/AccountActions.js @@ -0,0 +1,106 @@ +/* @flow */ +'use strict'; + +import * as ACCOUNT from './constants/account'; + +import { initialState } from '../reducers/AccountDetailReducer'; +import { findSelectedDevice } from '../reducers/TrezorConnectReducer'; + +import type { State } from '../reducers/AccountDetailReducer'; +import type { Discovery } from '../reducers/DiscoveryReducer'; + +export const init = (): any => { + return (dispatch, getState): void => { + + const { location } = getState().router; + const urlParams = location.params; + + const selected = findSelectedDevice( getState().connect ); + if (!selected) return; + + + const state: State = { + index: parseInt(urlParams.address), + checksum: selected.checksum, + coin: urlParams.coin, + location: location.pathname + }; + + dispatch({ + type: ACCOUNT.INIT, + state: state + }); + + + // let discoveryProcess: ?Discovery = getState().discovery.find(d => d.checksum === selected.checksum && d.coin === currentAccount.coin); + // const discovering: boolean = (!discoveryProcess || !discoveryProcess.completed); + + // const state: State = { + // ...initialState, + // loaded: true, + // checksum: currentAccount.checksum, + // address: currentAccount.address, + // coin: urlParams.coin, + // balance: currentAccount.balance, + + // discovering + // }; + + // dispatch({ + // type: ACCOUNT.INIT, + // state + // }); + } +} + +export const update = (newProps: any): any => { + return (dispatch, getState): void => { + + const { + accountDetail, + connect, + discovery, + accounts, + router + } = getState(); + + const isLocationChanged: boolean = newProps.location.pathname !== accountDetail.location; + + if (isLocationChanged) { + dispatch({ + type: ACCOUNT.INIT, + state: { + ...accountDetail, + location: newProps.location.pathname, + } + }); + return; + } + + // update comes from device + // const device = connect.devices.find(d => d.checksum === accountDetail.checksum); + // if (accountDetail.detail !== device) { + // console.warn("DEV UPDATE!!!!") + // } + + // const discoveryProcess = discovery.find(d => d.checksum === device.checksum && d.coin === accountDetail.coin); + + // const account = accounts.find(a => a.checksum === accountDetail.checksum && a.index === accountDetail.addressIndex && a.coin === accountDetail.coin); + // if (account && !accountDetail.address) { + // // update current address + // console.warn("ACC UPDATE!!!!") + // } + + + // isDeviceChanged + // isDiscoveryChanged + } +} + +export const dispose = (device: any): any => { + return (dispatch, getState): void => { + dispatch({ + type: ACCOUNT.DISPOSE, + }); + } +} \ No newline at end of file diff --git a/src/js/actions/AppActions.js b/src/js/actions/AppActions.js new file mode 100644 index 00000000..99d40154 --- /dev/null +++ b/src/js/actions/AppActions.js @@ -0,0 +1,33 @@ +/* @flow */ +'use strict'; + +export const ON_RESIZE: string = 'ON_RESIZE'; +export const ON_BEFORE_UNLOAD: string = 'app__on_before_unload'; +export const TOGGLE_DEVICE_DROPDOWN: string = 'TOGGLE_DEVICE_DROPDOWN'; +export const RESIZE_CONTAINER: string = 'RESIZE_CONTAINER'; + +export const onResize = (): any => { + return { + type: ON_RESIZE + } +} + +export const onBeforeUnload = (): any => { + return { + type: ON_BEFORE_UNLOAD + } +} + +export const resizeAppContainer = (opened: boolean): any => { + return { + type: RESIZE_CONTAINER, + opened + } +} + +export const toggleDeviceDropdown = (opened: boolean): any => { + return { + type: TOGGLE_DEVICE_DROPDOWN, + opened + } +} diff --git a/src/js/actions/DOMActions.js b/src/js/actions/DOMActions.js deleted file mode 100644 index 969c4f16..00000000 --- a/src/js/actions/DOMActions.js +++ /dev/null @@ -1,10 +0,0 @@ -/* @flow */ -'use strict'; - -export const ON_RESIZE: string = 'ON_RESIZE'; - -export const onResize = (): void => { - return { - type: ON_RESIZE - } -} diff --git a/src/js/actions/LocalStorageActions.js b/src/js/actions/LocalStorageActions.js new file mode 100644 index 00000000..e6d2203c --- /dev/null +++ b/src/js/actions/LocalStorageActions.js @@ -0,0 +1,112 @@ +/* @flow */ +'use strict'; + +import * as CONNECT from './constants/TrezorConnect'; +import * as ADDRESS from './constants/Address'; +import * as TOKEN from './constants/Token'; +import * as DISCOVERY from './constants/Discovery'; +import * as STORAGE from './constants/LocalStorage'; +import { httpRequest } from '../utils/networkUtils'; + +export function loadData(): any { + return async (dispatch, getState) => { + + // check if local storage is available + // let available: boolean = true; + // if (typeof window.localStorage === 'undefined') { + // available = false; + // } else { + // try { + // window.localStorage.setItem('ethereum_wallet', true); + // } catch (error) { + // available = false; + // } + // } + + dispatch( loadTokensFromJSON() ); + } +} + +export function loadTokensFromJSON(): any { + return async (dispatch, getState) => { + try { + const appConfig = await httpRequest('data/appConfig.json', 'json'); + const ethTokens = await httpRequest('data/ethTokens.json', 'json'); + const ethERC20 = await httpRequest('data/ethERC20.json', 'json'); + + const devices: ?string = get('devices'); + console.log("GET23", JSON.parse(devices)) + if (devices) { + dispatch({ + type: CONNECT.DEVICE_FROM_STORAGE, + payload: JSON.parse(devices) + }) + } + + const accounts: ?string = get('accounts'); + if (accounts) { + dispatch({ + type: ADDRESS.FROM_STORAGE, + payload: JSON.parse(accounts) + }) + } + + const tokens: ?string = get('tokens'); + if (tokens) { + dispatch({ + type: TOKEN.FROM_STORAGE, + payload: JSON.parse(tokens) + }) + } + + const discovery: ?string = get('discovery'); + if (discovery) { + dispatch({ + type: DISCOVERY.FROM_STORAGE, + payload: JSON.parse(discovery) + }) + } + + + dispatch({ + type: STORAGE.READY, + appConfig, + ethTokens, + ethERC20 + }) + + } catch(error) { + dispatch({ + type: STORAGE.ERROR, + error + }) + } + } +} + + +export const save = (key: string, value: string): any => { + return (dispatch, getState) => { + if (typeof window.localStorage !== 'undefined') { + //console.log("SAVEE!!!!", key, value) + try { + window.localStorage.setItem(key, value); + } catch (error) { + // available = false; + console.error("ERROR: " + error) + } + } + } +} + +export const get = (key: string): ?string => { + if (typeof window.localStorage !== 'undefined') { + try { + console.log("GETTT", JSON.parse(window.localStorage.getItem(key))) + return window.localStorage.getItem(key); + } catch (error) { + // available = false; + return null; + } + } +} \ No newline at end of file diff --git a/src/js/actions/ModalActions.js b/src/js/actions/ModalActions.js index 68fe9226..cd5b3446 100644 --- a/src/js/actions/ModalActions.js +++ b/src/js/actions/ModalActions.js @@ -3,125 +3,75 @@ import TrezorConnect, { UI, UI_EVENT } from 'trezor-connect'; import * as ACTIONS from './index'; +import * as MODAL from './constants/Modal'; +import * as CONNECT from './constants/TrezorConnect'; -export function onPinAdd(number: number): any { - return { - type: ACTIONS.ON_PIN_ADD, - number - } -} -export function onPinBackspace(): any { - return { - type: ACTIONS.ON_PIN_BACKSPACE - } -} - -export function onPinSubmit(value: string): void { +export function onPinSubmit(value: string): any { TrezorConnect.uiMessage({ type: UI.RECEIVE_PIN, data: value }); return { type: ACTIONS.CLOSE_MODAL } } -export function onPassphraseChange(value: string): any { - return { - type: ACTIONS.ON_PASSPHRASE_CHANGE, - value - } -} - -export function onPassphraseShow(): any { - return { - type: ACTIONS.ON_PASSPHRASE_SHOW - } -} - -export function onPassphraseHide(): any { - return { - type: ACTIONS.ON_PASSPHRASE_HIDE - } -} - -export function onPassphraseSave(): any { - return { - type: ACTIONS.ON_PASSPHRASE_SAVE +export function onPassphraseSubmit(passphrase: string): any { + return async (dispatch, getState): Promise => { + const resp = await TrezorConnect.uiMessage({ + type: UI.RECEIVE_PASSPHRASE, + data: { + value: passphrase, + save: true + } + }); + + dispatch({ + type: ACTIONS.CLOSE_MODAL + }); } } -export function onPassphraseForget(): any { +export const askForRemember = (device: any) => { return { - type: ACTIONS.ON_PASSPHRASE_FORGET + type: MODAL.REMEMBER, + device } } -export function onPassphraseFocus(): any { +export const onRememberDevice = (device: any) => { return { - type: ACTIONS.ON_PASSPHRASE_FOCUS + type: CONNECT.REMEMBER, + device } } -export function onPassphraseBlur(): any { +export const onForgetDevice = (device: any) => { return { - type: ACTIONS.ON_PASSPHRASE_BLUR + type: CONNECT.FORGET, + device, } } -export function onPassphraseSubmit(value: string, cache: boolean): void { - TrezorConnect.uiMessage({ - type: UI.RECEIVE_PASSPHRASE, - data: { - value, - save: cache - } - }); +export const onForgetSingleDevice = (device: any) => { return { - type: ACTIONS.CLOSE_MODAL + type: CONNECT.FORGET_SINGLE, + device, } } -export function onConfirmation(): any { - //postMessage(new UiMessage(UI.RECEIVE_CONFIRMATION, 'true') ); - TrezorConnect.uiMessage({ - type: UI.RECEIVE_CONFIRMATION, - data: 'true' - }); - +export const onCancel = () => { return { type: ACTIONS.CLOSE_MODAL } } -export function onConfirmationCancel(): any { - TrezorConnect.uiMessage({ - type: UI.RECEIVE_CONFIRMATION, - data: 'false' - }); - - return { - type: ACTIONS.CLOSE_MODAL - } -} +export const onDuplicateDevice = (device: any): any => { + return (dispatch: any, getState: any): void => { -export function onPermissionGranted(): any { - //postMessage(new UiMessage(UI.RECEIVE_CONFIRMATION, 'true') ); - TrezorConnect.uiMessage({ - type: UI.RECEIVE_PERMISSION, - data: 'true' - }); + dispatch( onCancel() ); - return { - type: ACTIONS.CLOSE_MODAL - } -} - -export function onPermissionRejected(): any { - TrezorConnect.uiMessage({ - type: UI.RECEIVE_PERMISSION, - data: 'false' - }); - - return { - type: ACTIONS.CLOSE_MODAL + dispatch({ + type: CONNECT.DUPLICATE, + device + }); } } \ No newline at end of file diff --git a/src/js/actions/ReceiveActions.js b/src/js/actions/ReceiveActions.js new file mode 100644 index 00000000..4ee0ee7c --- /dev/null +++ b/src/js/actions/ReceiveActions.js @@ -0,0 +1,110 @@ +/* @flow */ +'use strict'; + +import TrezorConnect from 'trezor-connect'; +import * as RECEIVE from './constants/receive'; +import * as NOTIFICATION from './constants/notification'; + +import { initialState } from '../reducers/ReceiveReducer'; +import type { State } from '../reducers/ReceiveReducer'; +import { findSelectedDevice } from '../reducers/TrezorConnectReducer'; + + +export const init = (): any => { + return (dispatch, getState): void => { + const { location } = getState().router; + const urlParams = location.params; + + const selected = findSelectedDevice( getState().connect ); + if (!selected) return; + + const state: State = { + ...initialState, + checksum: selected.checksum, + accountIndex: parseInt(urlParams.address), + coin: urlParams.coin, + location: location.pathname, + }; + + dispatch({ + type: RECEIVE.INIT, + state: state + }); + } +} + + +export const update = (newProps: any): any => { + return (dispatch, getState): void => { + const { + receive, + router + } = getState(); + + const isLocationChanged: boolean = router.location.pathname !== receive.location; + if (isLocationChanged) { + dispatch( init() ); + return; + } + } +} + +export const dispose = (address: string): any => { + return { + type: RECEIVE.DISPOSE + } +} + +export const showUnverifiedAddress = () => { + return { + type: RECEIVE.SHOW_UNVERIFIED_ADDRESS + } +} + +export const showAddress = (address_n: string): any => { + return async (dispatch, getState) => { + + const selected = findSelectedDevice(getState().connect); + + if (selected && !selected.connected) { + dispatch({ + type: RECEIVE.REQUEST_UNVERIFIED, + }); + return; + } + + const response = await TrezorConnect.ethereumGetAddress({ + device: { + path: selected.path, + instance: selected.instance, + state: selected.checksum + }, + address_n + }); + + if (response && response.success) { + dispatch({ + type: RECEIVE.SHOW_ADDRESS + }) + } else { + // TODO: handle invalid pin? + dispatch({ + type: NOTIFICATION.ADD, + payload: { + type: 'error', + title: 'Veryfying address error', + message: response.data.error, + cancelable: true, + actions: [ + { + label: 'Try again', + callback: () => { + dispatch(showAddress(address_n)) + } + } + ] + } + }) + } + } +} \ No newline at end of file diff --git a/src/js/actions/SendFormActions.js b/src/js/actions/SendFormActions.js index 1f7c791d..f923ce7c 100644 --- a/src/js/actions/SendFormActions.js +++ b/src/js/actions/SendFormActions.js @@ -1,80 +1,672 @@ /* @flow */ 'use strict'; -import * as ACTIONS from './index'; -import { getNonce, estimateGas, getGasPrice, push } from './Web3Actions'; +import * as SEND from './constants/SendForm'; +import * as NOTIFICATION from './constants/notification'; + +import { getNonce, estimateGas, getGasPrice, pushTx } from './Web3Actions'; import EthereumjsUtil from 'ethereumjs-util'; +import EthereumjsUnits from 'ethereumjs-units'; import EthereumjsTx from 'ethereumjs-tx'; import TrezorConnect from 'trezor-connect'; import { strip } from '../utils/ethUtils'; +import { push } from 'react-router-redux'; +import BigNumber from 'bignumber.js'; +import { initialState } from '../reducers/SendFormReducer'; +import type { State, FeeLevel } from '../reducers/SendFormReducer'; +import { findSelectedDevice } from '../reducers/TrezorConnectReducer'; -export const onAddressChange = (address: string): void => { - return { - type: ACTIONS.ON_ADDRESS_CHANGE, - address +const numberRegExp = new RegExp('^([0-9]{0,10}\\.)?[0-9]{1,18}$'); + +const calculateFee = (gasPrice: string, gasLimit: string): string => { + return EthereumjsUnits.convert( new BigNumber(gasPrice).times(gasLimit), 'gwei', 'ether'); +} + +const calculateTotal = (amount: string, gasPrice: string, gasLimit: string): string => { + try { + return new BigNumber(amount).plus( calculateFee(gasPrice, gasLimit) ).toString(); + } catch (error) { + return '0'; } } -export const onAmountChange = (amount: string): void => { - return { - type: ACTIONS.ON_AMOUNT_CHANGE, - amount +export const calculateMaxAmount = (balance: string, gasPrice: string, gasLimit: string): string => { + try { + const fee = EthereumjsUnits.convert( new BigNumber(gasPrice).times(gasLimit), 'gwei', 'ether'); + const b = new BigNumber(balance); + const max = b.minus(fee); + if (max.lessThan(0)) return '0'; + return max.toString(); + } catch (error) { + return '0'; } + } -export const onGasPriceChange = (gasPrice: string): void => { - return { - type: ACTIONS.ON_GAS_PRICE_CHANGE, - gasPrice +export const getFeeLevels = (coin: string, gasPrice: BigNumber | string, gasLimit: string): Array => { + if (typeof gasPrice === 'string') gasPrice = new BigNumber(gasPrice); + const quarter: BigNumber = gasPrice.dividedBy(4); + const high: string = gasPrice.plus(quarter.times(2)).toString(); + const low: string = gasPrice.minus(quarter.times(2)).toString(); + coin = coin.toUpperCase(); + + return [ + { + value: 'High', + gasPrice: high, + label: `${ calculateFee(high, gasLimit) } ${ coin }` + }, + { + value: 'Normal', + gasPrice: gasPrice.toString(), + label: `${ calculateFee(gasPrice.toString(), gasLimit) } ${ coin }` + }, + { + value: 'Low', + gasPrice: low, + label: `${ calculateFee(low, gasLimit) } ${ coin }` + }, + { + value: 'Custom', + gasPrice: low, + label: '', + }, + ] +} + +export const findBalance = (getState: any): string => { + const state = getState().sendForm; + const account = getState().accounts.find(a => a.checksum === state.checksum && a.index === state.accountIndex && a.coin === state.coin); + + if (state.token !== state.coin) { + return getState().tokens.find(t => t.ethAddress === account.address && t.symbol === state.token).balance; + } else { + return account.balance; } } -export const onGasLimitChange = (gasLimit: string): void => { + +// initialize component +export const init = (): any => { + return (dispatch, getState): void => { + + const { location } = getState().router; + const urlParams = location.params; + + const selected = findSelectedDevice( getState().connect ); + if (!selected) return; + + const web3instance = getState().web3.find(w3 => w3.coin === urlParams.coin); + if (!web3instance) { + // no backend for this coin + //return; + } + + // TODO: check if there are some unfinished tx in localStorage + const { config } = getState().localStorage; + const coin = config.coins.find(c => c.symbol === urlParams.coin); + + const gasPrice: BigNumber = new BigNumber( EthereumjsUnits.convert(web3instance.gasPrice, 'wei', 'gwei') ) || new BigNumber(coin.defaultGasPrice); + const gasLimit: string = coin.defaultGasLimit.toString(); + const feeLevels: Array = getFeeLevels(urlParams.coin, gasPrice, gasLimit); + + // TODO: get nonce + + const state: State = { + ...initialState, + checksum: selected.checksum, + accountIndex: parseInt(urlParams.address), + coin: urlParams.coin, + token: urlParams.coin, + location: location.pathname, + + feeLevels, + selectedFeeLevel: feeLevels.find(f => f.value === 'Normal'), + recommendedGasPrice: gasPrice.toString(), + gasLimit, + gasPrice: gasPrice.toString(), + nonce: '', // TODO!!! + }; + + dispatch({ + type: SEND.INIT, + state + }); + } +} + +export const update = (): any => { + return (dispatch, getState): void => { + const { + sendForm, + router + } = getState(); + + const isLocationChanged: boolean = router.location.pathname !== sendForm.location; + if (isLocationChanged) { + dispatch( init() ); + return; + } + } +} + +export const dispose = (): any => { return { - type: ACTIONS.ON_GAS_LIMIT_CHANGE, - gasLimit + type: SEND.DISPOSE } } -export const onTxDataChange = (data: string): void => { +export const toggleAdvanced = (address: string): any => { return { - type: ACTIONS.ON_TX_DATA_CHANGE, - data + type: SEND.TOGGLE_ADVANCED } } -export const onSend = (addressId): void => { - return async (dispatch, getState) => { - const { web3 } = getState().web3; - const { address, amount } = getState().sendForm; - const { addresses } = getState().addresses; +export const validation = (): any => { + return (dispatch, getState): void => { + + const state: State = getState().sendForm; + const errors: {[k: string]: string} = {}; + const warnings: {[k: string]: string} = {}; + const infos: {[k: string]: string} = {}; - const currentAddress = addresses[addressId]; - const address_n = currentAddress.path; + if (!state.untouched) { + + // valid address + if (state.touched.address) { + + const accounts = getState().accounts; + const myAccount = accounts.find(a => a.address.toLowerCase() === state.address.toLowerCase()); + + if (state.address.length < 1) { + errors.address = 'Address is not set'; + } else if (!EthereumjsUtil.isValidAddress(state.address)) { + errors.address = 'Address is not valid'; + } else if (myAccount) { + if (myAccount.coin === state.coin) { + infos.address = `TREZOR Address #${ (myAccount.index + 1) }`; + } else { + // TODO: load coins from config + warnings.address = `Looks like it's TREZOR address in Account #${ (myAccount.index + 1) } of ${ myAccount.coin.toUpperCase() }`; + } + } + } + + // valid amount + // https://stackoverflow.com/a/42701461 + //const regexp = new RegExp('^(?:[0-9]{0,10}\\.)?[0-9]{1,18}$'); + if (state.touched.amount) { + if (state.amount.length < 1) { + errors.amount = 'Amount is not set'; + } else if (state.amount.length > 0 && !state.amount.match(numberRegExp)) { + errors.amount = 'Amount is not a number'; + } else { + const account = getState().accounts.find(a => a.checksum === state.checksum && a.index === state.accountIndex && a.coin === state.coin); + if (state.token !== state.coin) { + const tokenBalance: string = getState().tokens.find(t => t.ethAddress === account.address && t.symbol === state.token).balance; + if (new BigNumber(state.total).greaterThan(account.balance)) { + errors.amount = `Not enough ${ state.coin.toUpperCase() } to cover transaction fee`; + } else if (new BigNumber(state.amount).greaterThan(tokenBalance)) { + errors.amount = 'Not enough funds'; + } + } else { + if (new BigNumber(state.total).greaterThan(account.balance)) { + errors.amount = 'Not enough funds'; + } + } + } + } + + // valid gas limit + if (state.touched.gasLimit) { + if (state.gasLimit.length < 1) { + errors.gasLimit = 'Gas limit is not set'; + } else if (state.gasLimit.length > 0 && !state.gasLimit.match(numberRegExp)) { + errors.gasLimit = 'Gas limit is not a number'; + } else { + const gl: BigNumber = new BigNumber(state.gasLimit); + if (gl.lessThan(1)) { + errors.gasLimit = 'Gas limit is too low'; + } else if (gl.lessThan(1000)) { + warnings.gasLimit = 'Gas limit is below recommended'; + } + } + } + + // valid gas price + if (state.touched.gasPrice) { + if (state.gasPrice.length < 1) { + errors.gasPrice = 'Gas price is not set'; + } else if (state.gasPrice.length > 0 && !state.gasPrice.match(numberRegExp)) { + errors.gasPrice = 'Gas price is not a number'; + } else { + const gp: BigNumber = new BigNumber(state.gasPrice); + if (gp.greaterThan(100)) { + errors.gasPrice = 'Gas price is too high'; + } else if (gp.lessThan(1)) { + errors.gasPrice = 'Gas price is too low'; + } + } + } + + // valid data + if (state.touched.data && state.coin === state.token && state.data.length > 0) { + const re = /^[0-9A-Fa-f]+$/g; + //const re = /^[0-9A-Fa-f]{6}$/g; + if (!re.test(state.data)) { + errors.data = 'Data is not valid hexadecimal'; + } + } + + // valid nonce? + + dispatch({ + type: SEND.VALIDATION, + errors, + warnings, + infos + }); + + } + } +} + + +export const onAddressChange = (address: string): any => { + return (dispatch, getState): void => { + + const currentState: State = getState().sendForm; + const touched = { ...currentState.touched }; + touched.address = true; + + const state: State = { + ...currentState, + untouched: false, + touched, + address + }; + + dispatch({ + type: SEND.ADDRESS_CHANGE, + state + }); + dispatch( validation() ); + } +} + +export const onAmountChange = (amount: string): any => { + return (dispatch, getState): void => { + + const currentState: State = getState().sendForm; + const touched = { ...currentState.touched }; + touched.amount = true; + const total: string = calculateTotal(currentState.token !== currentState.coin ? '0' : amount, currentState.gasPrice, currentState.gasLimit); + + const state: State = { + ...currentState, + untouched: false, + touched, + setMax: false, + amount, + total + }; + + dispatch({ + type: SEND.AMOUNT_CHANGE, + state + }); + dispatch( validation() ); + } +} + +export const onCurrencyChange = (currency: any): any => { + + return (dispatch, getState): void => { + + const currentState = getState().sendForm; + + const account = getState().accounts.find(a => a.checksum === currentState.checksum && a.index === currentState.accountIndex && a.coin === currentState.coin); + if (!account) { + // account not found + return; + } + + const { config } = getState().localStorage; + const coin = config.coins.find(c => c.symbol === currentState.coin); + + let gasLimit: string = ''; + let amount: string = currentState.amount; + let total: string; + + if (currentState.coin !== currency.value) { + gasLimit = coin.defaultGasLimitTokens.toString(); + if (currentState.setMax) { + const tokenBalance: string = getState().tokens.find(t => t.ethAddress === account.address && t.symbol === currency.value).balance; + amount = tokenBalance; + } + total = calculateTotal('0', currentState.gasPrice, currentState.gasLimit); + } else { + gasLimit = coin.defaultGasLimit.toString(); + if (currentState.setMax) { + amount = calculateMaxAmount(account.balance, currentState.gasPrice, currentState.gasLimit); + } + total = calculateTotal(amount, currentState.gasPrice, currentState.gasLimit); + } + + const feeLevels: Array = getFeeLevels(currentState.coin, currentState.gasPrice, gasLimit); + + const state: State = { + ...currentState, + token: currency.value, + amount, + total, + feeLevels, + selectedFeeLevel: feeLevels.find(f => f.value === currentState.selectedFeeLevel.value), + gasLimit, + }; + + dispatch({ + type: SEND.CURRENCY_CHANGE, + state + }); + dispatch( validation() ); + } +} + + + +export const onSetMax = (): any => { + return (dispatch, getState): void => { + const currentState = getState().sendForm; + const touched = { ...currentState.touched }; + touched.amount = true; + + const account = getState().accounts.find(a => a.checksum === currentState.checksum && a.index === currentState.accountIndex && a.coin === currentState.coin); + if (!account) { + // account not found + return; + } + + let amount: string = currentState.amount; + let total: string = currentState.total; + if (!currentState.setMax) { + if (currentState.token !== currentState.coin) { + const tokenBalance: string = getState().tokens.find(t => t.ethAddress === account.address && t.symbol === currentState.token).balance; + amount = tokenBalance; + total = calculateTotal('0', currentState.gasPrice, currentState.gasLimit); + } else { + amount = calculateMaxAmount(account.balance, currentState.gasPrice, currentState.gasLimit); + total = calculateTotal(amount, currentState.gasPrice, currentState.gasLimit); + } + } + + const state: State = { + ...currentState, + untouched: false, + touched, + setMax: !currentState.setMax, + amount, + total + }; + + dispatch({ + type: SEND.SET_MAX, + state + }); + dispatch( validation() ); + } +} + +export const onFeeLevelChange = (feeLevel: any): any => { + return (dispatch, getState): void => { + + const currentState = getState().sendForm; + const state: State = { + ...currentState, + untouched: false, + selectedFeeLevel: feeLevel, + }; + + if (feeLevel.value === 'Custom') { + // TODO: update value for custom fee + state.advanced = true; + feeLevel.gasPrice = state.gasPrice; + feeLevel.label = `${ calculateFee(state.gasPrice, state.gasLimit) } ${ state.coin.toUpperCase() }`; + } else { + const customLevel = state.feeLevels.find(f => f.value === 'Custom'); + customLevel.label = ''; + state.gasPrice = feeLevel.gasPrice; + } + + if (currentState.setMax) { + const account = getState().accounts.find(a => a.checksum === currentState.checksum && a.index === currentState.accountIndex && a.coin === currentState.coin); + if (state.token !== state.coin) { + const tokenBalance: string = getState().tokens.find(t => t.ethAddress === account.address && t.symbol === currentState.token).balance; + state.amount = tokenBalance; + } else { + state.amount = calculateMaxAmount(account.balance, state.gasPrice, state.gasLimit); + } + } + state.total = calculateTotal(state.token !== state.coin ? '0' : state.amount, state.gasPrice, state.gasLimit); + + dispatch({ + type: SEND.FEE_LEVEL_CHANGE, + state + }); + dispatch( validation() ); + } +} + +export const updateFeeLevels = (): any => { + return (dispatch, getState): void => { + const currentState = getState().sendForm; + const feeLevels: Array = getFeeLevels(currentState.coin, currentState.recommendedGasPrice, currentState.gasLimit); + const state: State = { + ...currentState, + feeLevels, + selectedFeeLevel: feeLevels.find(f => f.value === currentState.selectedFeeLevel.value), + gasPrice: currentState.recommendedGasPrice, + gasPriceNeedsUpdate: false, + }; + + if (currentState.setMax) { + const account = getState().accounts.find(a => a.checksum === currentState.checksum && a.index === currentState.accountIndex && a.coin === currentState.coin); + if (state.token !== state.coin) { + const tokenBalance: string = getState().tokens.find(t => t.ethAddress === account.address && t.symbol === currentState.token).balance; + state.amount = tokenBalance; + } else { + state.amount = calculateMaxAmount(account.balance, state.gasPrice, state.gasLimit); + } + } + state.total = calculateTotal(state.token !== state.coin ? '0' : state.amount, state.gasPrice, state.gasLimit); + + dispatch({ + type: SEND.UPDATE_FEE_LEVELS, + state + }); + dispatch( validation() ); + } +} + +export const onGasPriceChange = (gasPrice: string): any => { + return (dispatch, getState): void => { + + const currentState = getState().sendForm; + const touched = { ...currentState.touched }; + touched.gasPrice = true; + + const state: State = { + ...currentState, + untouched: false, + touched, + gasPrice: gasPrice, + }; + + if (gasPrice.match(numberRegExp) && state.gasLimit.match(numberRegExp)) { + const customLevel = currentState.feeLevels.find(f => f.value === 'Custom'); + customLevel.gasPrice = gasPrice; + customLevel.label = `${ calculateFee(gasPrice, state.gasLimit) } ${ state.coin.toUpperCase() }`; + + state.selectedFeeLevel = customLevel; + + if (currentState.setMax) { + const account = getState().accounts.find(a => a.checksum === currentState.checksum && a.index === currentState.accountIndex && a.coin === currentState.coin); + if (state.token !== state.coin) { + const tokenBalance: string = getState().tokens.find(t => t.ethAddress === account.address && t.symbol === currentState.token).balance; + state.amount = tokenBalance; + } else { + state.amount = calculateMaxAmount(account.balance, state.gasPrice, state.gasLimit); + } + } + } + + state.total = calculateTotal(state.token !== state.coin ? '0' : state.amount, state.gasPrice, state.gasLimit); + + dispatch({ + type: SEND.GAS_PRICE_CHANGE, + state + }); + dispatch( validation() ); + } +} + +export const onGasLimitChange = (gasLimit: string): any => { + return (dispatch, getState): void => { + const currentState = getState().sendForm; + const touched = { ...currentState.touched }; + touched.gasLimit = true; + + const state: State = { + ...currentState, + untouched: false, + touched, + gasLimit, + }; + + if (gasLimit.match(numberRegExp) && state.gasPrice.match(numberRegExp)) { + const customLevel = currentState.feeLevels.find(f => f.value === 'Custom'); + customLevel.label = `${ calculateFee(state.gasPrice, gasLimit) } ${ state.coin.toUpperCase() }`; + + state.selectedFeeLevel = customLevel; + + if (currentState.setMax) { + const account = getState().accounts.find(a => a.checksum === currentState.checksum && a.index === currentState.accountIndex && a.coin === currentState.coin); + if (state.token !== state.coin) { + const tokenBalance: string = getState().tokens.find(t => t.ethAddress === account.address && t.symbol === currentState.token).balance; + state.amount = tokenBalance; + } else { + state.amount = calculateMaxAmount(account.balance, state.gasPrice, state.gasLimit); + } + } + } + + state.total = calculateTotal(state.token !== state.coin ? '0' : state.amount, state.gasPrice, state.gasLimit); + + dispatch({ + type: SEND.GAS_LIMIT_CHANGE, + state + }); + dispatch( validation() ); + } +} + +export const onDataChange = (data: string): any => { + return (dispatch, getState): void => { + const currentState = getState().sendForm; + const touched = { ...currentState.touched }; + touched.data = true; + + const state: State = { + ...currentState, + untouched: false, + touched, + data, + }; + + dispatch({ + type: SEND.DATA_CHANGE, + state + }); + dispatch( validation() ); + } +} + +export const onSend = (): any => { + //return onSendERC20(); + + return async (dispatch, getState): Promise => { + + const state: State = getState().sendForm; + const web3instance = getState().web3.filter(w3 => w3.coin === state.coin)[0]; + const web3 = web3instance.web3; + + const account = getState().accounts.find(a => a.checksum === state.checksum && a.index === state.accountIndex && a.coin === state.coin); + + const address_n = account.addressPath; + + let data: string = ''; + let txAmount = web3.toHex(web3.toWei(state.amount, 'ether')); + let txAddress = state.address; + if (state.coin !== state.token) { + const tokens = getState().tokens + const t = tokens.find(t => t.ethAddress === account.address && t.symbol === state.token); + const contract = web3instance.erc20.at(t.address); + data = contract.transfer.getData(state.address, state.amount, { + from: account.address, + gasLimit: state.gasLimit, + gasPrice: state.gasPrice + }); + txAmount = '0x00'; + txAddress = t.address; + } + + const txData = { address_n, - to: address, - value: web3.toHex(web3.toWei(amount, 'ether')), - data: '', - chainId: 3 + // from: currentAddress.address + to: txAddress, + value: txAmount, + data, + //chainId: 3 // ropsten + chainId: web3instance.chainId, + nonce: web3.toHex(account.nonce), + gasLimit: web3.toHex(state.gasLimit), + gasPrice: web3.toHex( EthereumjsUnits.convert(state.gasPrice, 'gwei', 'wei') ), + r: '', + s: '', + v: '' } - const nonce = await getNonce(web3, currentAddress.address); - const gasOptions = { - to: txData.to, - data: txData.data - } - const gasLimit = await estimateGas(web3, gasOptions); - const gasPrice = await getGasPrice(web3); + //const nonce = await getNonce(web3, currentAddress.address); + //txData.nonce = web3.toHex(nonce); + + + // const gasOptions = { + // to: txData.to, + // data: txData.data + // } + + // const gasPrice = await getGasPrice(web3); - txData.nonce = web3.toHex(nonce); - txData.gasLimit = web3.toHex(gasLimit); - txData.gasPrice = web3.toHex(gasPrice); + + + // txData.nonce = web3.toHex(nonce); + // txData.gasLimit = web3.toHex(gasLimit); + // txData.gasPrice = web3.toHex( EthereumjsUnits.convert(gasPrice, 'gwei', 'wei') ); + + // console.log("---->GASSS", txData, gasLimit, gasPrice, EthereumjsUnits.convert(gasPrice, 'gwei', 'wei')); + + const selected = findSelectedDevice(getState().connect); let signedTransaction = await TrezorConnect.ethereumSignTransaction({ + device: { + path: selected.path, + instance: selected.instance, + state: selected.checksum + }, //path: "m/44'/60'/0'/0/0", address_n: txData.address_n, nonce: strip(txData.nonce), @@ -82,24 +674,85 @@ export const onSend = (addressId): void => { gas_limit: strip(txData.gasLimit), to: strip(txData.to), value: strip(txData.value), - data: txData.data, + data: strip(txData.data), chain_id: txData.chainId }); + if (!signedTransaction || !signedTransaction.success) { + + dispatch({ + type: NOTIFICATION.ADD, + payload: { + type: 'error', + title: 'Transaction error', + message: signedTransaction.data.error, + cancelable: true, + actions: [ ] + } + }) + return; + } + txData.r = '0x' + signedTransaction.data.r; txData.s = '0x' + signedTransaction.data.s; txData.v = web3.toHex(signedTransaction.data.v); - const tx = new EthereumjsTx(txData); - const serializedTx = '0x' + tx.serialize().toString('hex'); + const gasLimit2 = await estimateGas(web3, txData); + console.log("---->GASSS", txData, gasLimit2.toString() ); + + try { + const tx = new EthereumjsTx(txData); + const serializedTx = '0x' + tx.serialize().toString('hex'); + const txid = await pushTx(web3, serializedTx); + + dispatch({ + type: SEND.TX_COMPLETE, + address: account, + txid, + txData, + }); + + dispatch({ + type: NOTIFICATION.ADD, + payload: { + type: 'success', + title: 'Transaction success', + message: `detail`, + cancelable: true, + actions: [] + } + }); + + } catch(error) { - const txid = await push(web3, serializedTx); + dispatch({ + type: NOTIFICATION.ADD, + payload: { + type: 'error', + title: 'Transaction error', + message: error.message || error, + cancelable: true, + actions: [ ] + } + }); + } + + // const tx = new EthereumjsTx(txData); + // console.log("2222", tx, tx.toJSON(), tx.from, tx.to); + // const serializedTx = '0x' + tx.serialize().toString('hex'); + + // console.log("----> PUSZ TX", web3, currentAddress, serializedTx) + // const txid = await pushTx(web3, serializedTx); + // console.log("----> PUSZ TX2", web3, serializedTx) - dispatch({ - type: ACTIONS.ON_TX_COMPLETE, - address: currentAddress, - txid, - txData, - }) + // dispatch({ + // type: SEND.TX_COMPLETE, + // address: currentAddress, + // txid, + // txData, + // }) + + // const [ url ] = getState().router.location.pathname.split('/send'); + // dispatch( push(url) ); } } diff --git a/src/js/actions/SummaryActions.js b/src/js/actions/SummaryActions.js new file mode 100644 index 00000000..6a2ad931 --- /dev/null +++ b/src/js/actions/SummaryActions.js @@ -0,0 +1,282 @@ +/* @flow */ +'use strict'; + +import EthereumjsUtil from 'ethereumjs-util'; +import * as ACTIONS from './index'; +import * as SUMMARY from './constants/summary'; +import * as TOKEN from './constants/Token'; +import * as ADDRESS from './constants/Address'; +import { resolveAfter } from '../utils/promiseUtils'; +import { getTokenInfoAsync, getTokenBalanceAsync } from './Web3Actions'; + +import { initialState } from '../reducers/SummaryReducer'; +import type { State } from '../reducers/SummaryReducer'; +import { findSelectedDevice } from '../reducers/TrezorConnectReducer'; + + +export const init = (): any => { + return (dispatch, getState): void => { + const { location } = getState().router; + const urlParams = location.params; + + const selected = findSelectedDevice( getState().connect ); + if (!selected) return; + + const state: State = { + ...initialState, + checksum: selected.checksum, + accountIndex: parseInt(urlParams.address), + coin: urlParams.coin, + location: location.pathname, + }; + + dispatch({ + type: SUMMARY.INIT, + state: state + }); + } +} + + +export const update = (newProps: any): any => { + return (dispatch, getState): void => { + const { + summary, + router + } = getState(); + + const isLocationChanged: boolean = router.location.pathname !== summary.location; + if (isLocationChanged) { + dispatch( init() ); + return; + } + } +} + +export const dispose = (address: string): any => { + return { + type: SUMMARY.DISPOSE + } +} + +export const onDetailsToggle = (): any => { + return { + type: SUMMARY.DETAILS_TOGGLE + } +} + +// export const init = (address: string): any => { +// return (dispatch, getState): void => { +// const { location } = getState().router; +// const urlParams = location.params; + +// const selected = findSelectedDevice(getState().connect); +// const accounts = getState().accounts; +// // const currentAccount = accounts.find(a => a.index === parseInt(urlParams.address) && a.coin === urlParams.coin && a.deviceId === urlParams.device && a.loaded); +// const currentAccount = accounts.find(a => a.index === parseInt(urlParams.address) && a.coin === urlParams.coin && a.checksum === selected.checksum); +// if (!currentAccount) { +// console.log("STATER", getState()) +// // account not found +// return; +// } + +// const web3instance = getState().web3.find(w3 => w3.coin === urlParams.coin); +// if (!web3instance) { +// // no backend for this coin +// return; +// } + +// const state: State = { +// ...initialState, +// loaded: true, +// address: currentAccount.address, +// coin: urlParams.coin, +// balance: currentAccount.balance, +// }; + + +// dispatch({ +// type: SUMMARY.INIT, +// state +// }); + +// } +// } + + + + +export const loadTokens = (input: string): any => { + return async (dispatch, getState): Promise => { + + if (input.length < 1) return null; + + const { ethTokens } = getState().localStorage; + + const value = input.toLowerCase(); + const result = ethTokens.filter(t => + t.symbol.toLowerCase().indexOf(value) >= 0 || + t.address.toLowerCase().indexOf(value) >= 0 || + t.name.toLowerCase().indexOf(value) >= 0 + ); + //const result = ethTokens.filter(t => t.symbol.toLowerCase().indexOf(lower) >= 0); + + console.log("RESULT!", result.length, result) + + if (result.length > 0) { + return { options: result }; + } else { + const web3instance = getState().web3.find(w3 => w3.coin === 'eth'); + + const info = await getTokenInfoAsync(web3instance.erc20, input); + info.address = input; + + console.log("FETCH", info) + + if (info) { + return { + options: [ info ] + } + } else { + return { + + } + } + + + //await resolveAfter(300000); + //await resolveAfter(3000); + + + + + } + + } +} + +export const selectToken = (token: any, account: any): any => { + return async (dispatch, getState): Promise => { + + console.warn("ADD", token, account) + + const web3instance = getState().web3.find(w3 => w3.coin === account.coin); + + dispatch({ + type: TOKEN.ADD, + payload: { + ...token, + ethAddress: account.address, + checksum: account.checksum + } + }); + + // TODO: load token balance + const tokenBalance = await getTokenBalanceAsync(web3instance.erc20, token.address, account.address); + dispatch({ + type: TOKEN.SET_BALANCE, + payload: { + ethAddress: account.address, + address: token.address, + balance: tokenBalance.toString() + } + }) + + } +} + + + +export const onTokenSearch = (search: string): any => { + return { + type: ACTIONS.TOKENS_SEARCH, + search + } +} + +export const onCustomTokenToggle = (): any => { + return { + type: ACTIONS.TOKENS_CUSTOM_TOGGLE + } +} + +export const onCustomTokenAddressChange = (value: string): any => { + // todo: + // -validate addres + // - if adress is ok, try to fetch token info + // - check if addres does not exist in predefined coins + // return { + // type: ACTIONS.TOKENS_CUSTOM_ADDRESS_CHANGE, + // value + // } + + return async (dispatch, getState) => { + + const valid: boolean = EthereumjsUtil.isValidAddress(value); + if (valid) { + + dispatch({ + type: ACTIONS.TOKENS_CUSTOM_ADDRESS_CHANGE, + value, + valid, + fetching: true + }); + + const { web3, abi } = getState().web3; + const contract = web3.eth.contract(abi).at(value); + + contract.name.call((error, name) => { + if (error) { + // TODO: skip + } + contract.symbol.call((error, symbol) => { + if (error) { + // TODO: skip + } + + contract.decimals.call((error, decimals) => { + console.log("fetched!", name, symbol, decimals) + }) + }); + + + }) + + } else { + dispatch({ + type: ACTIONS.TOKENS_CUSTOM_ADDRESS_CHANGE, + value, + valid + }); + } + + console.log("VALID!!!", valid); + } +} + +export const onCustomTokenNameChange = (value: string): any => { + return { + type: ACTIONS.TOKENS_CUSTOM_NAME_CHANGE, + value + } +} + +export const onCustomTokenShortcutChange = (value: string): any => { + return { + type: ACTIONS.TOKENS_CUSTOM_SHORTCUT_CHANGE, + value + } +} + +export const onCustomTokenDecimalChange = (value: string): any => { + return { + type: ACTIONS.TOKENS_CUSTOM_DECIMAL_CHANGE, + value + } +} + +export const onCustomTokenAdd = (): any => { + return { + type: ACTIONS.TOKENS_CUSTOM_ADD + } +} \ No newline at end of file diff --git a/src/js/actions/TrezorConnectActions.1.js b/src/js/actions/TrezorConnectActions.1.js deleted file mode 100644 index 203d7a46..00000000 --- a/src/js/actions/TrezorConnectActions.1.js +++ /dev/null @@ -1,260 +0,0 @@ -/* @flow */ -'use strict'; - -import TrezorConnect, { UI } from 'trezor-connect'; -import * as ACTIONS from './index'; - -//import wallet from 'ethereumjs-wallet'; -//import hdkey from 'ethereumjs-wallet/hdkey'; -import HDKey from 'hdkey'; -import ethUtil from 'ethereumjs-util'; -import EthereumjsTx from 'ethereumjs-tx'; -import * as ethereumUtils from '../utils/ethUtils'; -import { hexToString, stringToHex } from '../utils/formatUtils'; -import * as Web3Actions from './Web3Actions'; - -export function onSelectDevice2(path: string): any { - return { - type: ACTIONS.ON_SELECT_DEVICE, - path - } -} - -export function discover(txData): any { - return async function (dispatch) { - let response = await TrezorConnect.getPublicKey({ path: "m/44'/60'/0'/0", confirmation: false }); - dispatch({ - type: 'create_account', - xpub: response.data.xpub, - publicKey: response.data.publicKey, - chainCode: response.data.chainCode, - path: response.data.path - }) - } -} - -export function signTx(txData): any { - return async function (dispatch) { - - console.log("RESP2", txData) - - //txData.nonce = "0x01"; - //txData.gasPrice = "0x02540be400"; - // txData.gasLimit = "0x5208"; - // txData.value = "0x5af3107a4000"; - - let response = await TrezorConnect.ethereumSignTransaction({ - //path: "m/44'/60'/0'/0/0", - address_n: txData.address_n, - nonce: ethereumUtils.strip(txData.nonce), - gas_price: ethereumUtils.strip(txData.gasPrice), - gas_limit: ethereumUtils.strip(txData.gasLimit), - to: ethereumUtils.strip(txData.to), - value: ethereumUtils.strip(txData.value), - data: txData.data, - chain_id: txData.chainId - }); - - txData.r = '0x' + response.data.r; - txData.s = '0x' + response.data.s; - txData.v = web3.toHex(response.data.v); - - console.log("RESP2", response, txData) - - const tx = new EthereumjsTx(txData); - var signedTx = '0x' + tx.serialize().toString('hex'); - - - const rawTx2 = { - "nonce":"0x01", - "gasPrice":"0x02540be400", - "gasLimit":"0x5208", - "to":"0x7314e0f1C0e28474bDb6be3E2c3E0453255188f8", - "value":"0x5af3107a4000", - "data":"", - "chainId":3, - "v":"0x2a", - "r":"0x210af4e1698f0437125424ac378da7304dea94dde34cbb57b62624069ceae969", - "s":"0x74c885c3d32330d63e32dff3b79a302ae5ed9b9abcf6e903fd32cbb91d94b6df" - } - - var tx2 = new EthereumjsTx(rawTx2); - var signedTx2 = '0x' + tx2.serialize().toString('hex'); - - console.log(signedTx) - console.log(signedTx2) - console.log(signedTx === signedTx2) - console.log(txData, rawTx2) - - // web3.eth.sendRawTransaction(signedTx, function(a1, a2){ - // console.log("SIGNEEED", a1, a2) - // }) - } -} - -export function onSelectDevice(): any { - return async function (dispatch, getState) { - dispatch(Web3Actions.composeTransaction()); - } -} -export function onSelectDeviceWeb3(): any { - return async function (dispatch, getState) { - - const { web3 } = getState().web3; - - console.log("WEB3333", web3) - - let resp = await TrezorConnect.getPublicKey({ path: "m/44'/60'/0'/0", confirmation: false }); - - let hdk = new HDKey(); - hdk.publicKey = new Buffer(resp.data.publicKey, 'hex'); - hdk.chainCode = new Buffer(resp.data.chainCode, 'hex'); - - var derivedKey = hdk.derive("m/0"); - - var address = ethUtil.publicToAddress(derivedKey.publicKey, true); - - // balance 0.100100000000000000 eth - var txData = { - address_n: [ - (44 | 0x80000000) >>> 0, - (60 | 0x80000000) >>> 0, - (0 | 0x80000000) >>> 0, - 0, 0 - ], - to: '0x7314e0f1C0e28474bDb6be3E2c3E0453255188f8', - value: web3.toHex(web3.toWei('0.0001', 'ether')), - data: '', - chainId: 3 - } - - web3.eth.getTransactionCount('0x' + address.toString('hex'), (error, result) => { - txData.nonce = web3.toHex(result); - const gasOptions = { - to: txData.to, - data: txData.data - } - web3.eth.estimateGas(gasOptions, (error, result) => { - txData.gasLimit = web3.toHex(result); - - web3.eth.getGasPrice(function(error, result){ - if (error) throw error; - - txData.gasPrice = web3.toHex(result); - console.warn("gesgas", error, result.toString(10), txData) - - dispatch( signTx(txData) ); - }); - }); - }); - - - - - - - - // web3.eth.getTransactionCount('0x' + address.toString('hex'), (error, result) => { - - // if (error) throw error; - - // console.log("getTransactionCount", result) - - // }); - - // let gas = { - // to: "0x7314e0f1C0e28474bDb6be3E2c3E0453255188f8", - // data: "" - // }; - - // web3.eth.estimateGas(gas, function(error, result){ - // console.warn("estimategas", error, result, result.toString(10)) - - // web3.eth.getGasPrice(function(error, result){ - // console.warn("gesgas", error, result.toString(10) result.) - // }) - - // }); - - - //console.log("SIGNEEED", signedTx); - - - //console.log("HDK", derivedKey, address.toString('hex'), rawTx, txData, hexToString(txData.value)) - //console.log("HDK", derivedKey, hexToString(txData.value), txData ) - - // const hd = hdkey.fromExtendedKey(resp.data.xpub); - // console.log("HDKI!", hd) - - - // var balance = web3.eth.getBalance('0x' + address.toString('hex'), function(error, result){ - // if(!error) - // console.log("res", result, result.toString(10) ) - // else - // console.error("erro", error); - // }); - - - - - - // web3.eth.sendRawTransaction(signedTx, function(a1, a2){ - // console.log("SIGNEEED", a1, a2) - // }) - //console.log(web3.eth) - //web3.eth.sendRawTransaction(signedTx).on('receipt', console.log); - - // web3.eth.getTransactionCount('0x' + address.toString('hex'), function(error, result) { - // //web3.eth.getTransactionCount("0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe", function(error, result) { - // console.log("getTransactionCount", error, result) - // }) - - // web3.eth.getAccounts(console.log) - - // const nonce = '03'; // note - it is hex, not number!!! - // const gas_price = '098bca5a00'; - // const gas_limit = 'a43f'; - // const to = 'e0b7927c4af23765cb51314a0e0521a9645f0e2a'; - // // var value = '01'; // in hexadecimal, in wei - this is 1 wei - // const value = '010000000000000000'; // in hexadecimal, in wei - this is about 18 ETC - // const data = 'a9059cbb000000000000000000000000dc7359317ef4cc723a3980213a013c0433a338910000000000000000000000000000000000000000000000000000000001312d00'; // some contract data - // // var data = null // for no data - // const chain_id = 1; // 1 for ETH, 61 for ETC - - // let resp2 = await TrezorConnect.ethereumSignTransaction({ - // address_n, - // nonce, - // gas_price, - // gas_limit, - // to, - // value, - // data, - // chain_id - // }); - - - - - // old fallback - // this.signEthereumTx = function ( - // address_n, - // nonce, - // gas_price, - // gas_limit, - // to, - // value, - // data, - // chain_id, - // callback, - // requiredFirmware - // ) - - // var coinbase = web3.eth.coinbase; - // var balance = web3.eth.getBalance(coinbase); - // console.log(balance.toString(10)); - - // dispatch({ - // type: 'DDD' - // }); - } -} \ No newline at end of file diff --git a/src/js/actions/TrezorConnectActions.js b/src/js/actions/TrezorConnectActions.js index 7eb448bb..d12f2a7f 100644 --- a/src/js/actions/TrezorConnectActions.js +++ b/src/js/actions/TrezorConnectActions.js @@ -1,95 +1,710 @@ /* @flow */ 'use strict'; -import TrezorConnect, { UI } from 'trezor-connect'; +import TrezorConnect, { UI, DEVICE, DEVICE_EVENT, UI_EVENT } from 'trezor-connect'; import * as ACTIONS from './index'; +import * as ADDRESS from './constants/Address'; +import * as TOKEN from './constants/Token'; +import * as CONNECT from './constants/TrezorConnect'; +import * as DISCOVERY from './constants/Discovery'; +import * as NOTIFICATION from './constants/notification'; -//import wallet from 'ethereumjs-wallet'; -//import hdkey from 'ethereumjs-wallet/hdkey'; import HDKey from 'hdkey'; import EthereumjsUtil from 'ethereumjs-util'; import EthereumjsTx from 'ethereumjs-tx'; -import { Address } from '../reducers/AddressesReducer'; -import { getBalance } from '../services/Web3Service'; +//import { getBalance } from '../services/Web3Service'; +import { getBalance } from './Web3Actions'; import { getTransactionHistory } from '../services/EtherscanService'; import { push } from 'react-router-redux'; -export function onSelectDevice2(path: string): any { - return { - type: ACTIONS.ON_SELECT_DEVICE, - path +import { init as initWeb3, getNonce, getBalanceAsync, getTokenBalanceAsync } from './Web3Actions'; + +import type { Discovery } from '../reducers/DiscoveryReducer'; +import { resolveAfter } from '../utils/promiseUtils'; +import { getAccounts } from '../utils/reducerUtils'; +import { findSelectedDevice, isSavedDevice } from '../reducers/TrezorConnectReducer'; + + + +export const init = (): any => { + return async (dispatch, getState): Promise => { + // set listeners + TrezorConnect.on(DEVICE_EVENT, (event: DeviceMessage): void => { + dispatch({ + type: event.type, + device: event.data + }); + }); + + const version: Object = TrezorConnect.getVersion(); + if (version.type === 'library') { + // handle UI events only if TrezorConnect isn't using popup + TrezorConnect.on(UI_EVENT, (type: string, data: any): void => { + // post event to reducers + dispatch({ + type, + data + }); + }); + } + + try { + await TrezorConnect.init({ + transport_reconnect: true, + }); + + setTimeout(() => { + dispatch( initWeb3() ); + }, 2000) + + } catch (error) { + dispatch({ + type: CONNECT.INITIALIZATION_ERROR, + error + }) + } } } -export function remove(devicePath): any { +// called after backend was initialized +// set listeners for connect/disconnect +export const postInit = (): any => { + return (dispatch, getState): void => { + const handleDeviceConnect = (device) => { + dispatch( initConnectedDevice(device) ); + } + + // const handleDeviceDisconnect = (device) => { + // // remove addresses and discovery from state + // // dispatch( remove(device) ); + // } + + TrezorConnect.on(DEVICE.CONNECT, handleDeviceConnect); + TrezorConnect.on(DEVICE.CONNECT_UNACQUIRED, handleDeviceConnect); + + // TrezorConnect.on(DEVICE.DISCONNECT, handleDeviceDisconnect); + // TrezorConnect.on(DEVICE.CONNECT_UNACQUIRED, handleDeviceDisconnect); + + // possible race condition: + // devices were connected before Web3 initialized. force DEVICE.CONNECT event on them + const { devices } = getState().connect; + + if (devices.length > 0) { + const unacquired = devices.find(d => d.unacquired); + if (unacquired) { + handleDeviceConnect(unacquired); + } else { + const latest = devices.sort((a, b) => { + if (!a.ts || !b.ts) { + return -1; + } else { + return a.ts > b.ts ? 1 : -1; + } + }); + + console.log("LATEST", latest) + } + } + for (let d of devices) { + handleDeviceConnect(d); + } + } +} + +export const initConnectedDevice = (device: any): any => { + return (dispatch, getState): void => { + + const selected = findSelectedDevice(getState().connect); + if (device.unacquired && selected && selected.path !== device.path && !selected.connected) { + dispatch( onSelectDevice(device) ); + } else if (!selected) { + dispatch( onSelectDevice(device) ); + } + } +} + +// selection from Aside dropdown +// or after acquiring device +export function onSelectDevice(device: any): any { + return (dispatch, getState): void => { + // || device.isUsedElsewhere + if (device.unacquired) { + dispatch( push(`/device/${ device.path }/acquire`) ); + } else if (device.features.bootloader_mode) { + dispatch( push(`/device/${ device.path }/bootloader`) ); + } else if (device.instance) { + dispatch( push(`/device/${ device.features.device_id }:${ device.instance }`) ); + } else { + //dispatch( push(`/device/${ device.features.device_id }/coin/etc/address/0`) ); + dispatch( push(`/device/${ device.features.device_id }`) ); + } + } +} + +// TODO: as TrezorConnect method +const __getDeviceState = async (path, instance): Promise => { + return await TrezorConnect.getPublicKey({ + device: { + path, + instance + }, + // selectedDevice: path, + instance: instance, + path: "m/1'/0'/0'", + confirmation: false + }); +} + +export const switchToFirstAvailableDevice = (): any => { + return async (dispatch, getState): Promise => { + + const { devices } = getState().connect; + if (devices.length > 0) { + // TODO: Priority: + // 1. Unacquired + // 2. First connected + // 3. Saved with latest timestamp + // 4. First from the list + const unacquired = devices.find(d => d.unacquired); + if (unacquired) { + dispatch( initConnectedDevice(unacquired) ); + } else { + const latest = devices.sort((a, b) => { + if (!a.ts || !b.ts) { + return -1; + } else { + return a.ts > b.ts ? 1 : -1; + } + }); + dispatch( initConnectedDevice(devices[0]) ); + } + + } else { + dispatch( push('/') ); + dispatch({ + type: CONNECT.SELECT_DEVICE, + payload: null + }) + } + } +} + +export const getSelectedDeviceState = (): any => { + return async (dispatch, getState): Promise => { + const selected = findSelectedDevice(getState().connect); + console.warn("init selected", selected) + if (selected + && selected.connected + && !selected.unacquired + && !selected.acquiring + && !selected.checksum) { + + const response = await __getDeviceState(selected.path, selected.instance); + + if (response && response.success) { + dispatch({ + type: CONNECT.AUTH_DEVICE, + device: selected, + checksum: response.data.xpub + }); + } else { + dispatch({ + type: NOTIFICATION.ADD, + payload: { + type: 'error', + title: 'Authentification error', + message: response.data.error, + cancelable: true, + actions: [ + { + label: 'Try again', + callback: () => { + dispatch( getSelectedDeviceState() ); + } + } + ] + } + }) + } + } + } +} + +export const deviceDisconnect = (device: any): any => { + return async (dispatch, getState): Promise => { + + if (!device || !device.features) return null; + + const selected = findSelectedDevice(getState().connect); + if (selected && selected.features.device_id === device.features.device_id) { + stopDiscoveryProcess(selected); + } + + const affected = getState().connect.devices.filter(d => d.features && d.checksum && !d.remember && d.features.device_id === device.features.device_id); + if (affected.length > 0) { + dispatch({ + type: CONNECT.REMEMBER_REQUEST, + device, + allInstances: affected + }); + } + + + // if (selected && selected.checksum) { + // if (device.features && device.features.device_id === selected.features.device_id) { + // stopDiscoveryProcess(selected); + // } + // } + + + + // // stop running discovery process on this device + // if (selected && selected.path === device.path){ + // if (selected.checksum) { + // stopDiscoveryProcess(selected); + // } + // } + + // // check if disconnected device was remembered before. + // // request modal if not + // const affected = getState().connect.devices.filter(d => d.path === device.path && d.checksum && !device.remember); + + + // check if reload is needed + if (!selected) { + dispatch( switchToFirstAvailableDevice() ); + } + } +} + +export const coinChanged = (coin: ?string): any => { + return (dispatch, getState): void => { + const selected = findSelectedDevice(getState().connect); + dispatch( stopDiscoveryProcess(selected) ); + + if (coin) { + dispatch( startDiscoveryProcess(selected, coin) ); + } + } +} + + +export function acquire(): any { return async (dispatch, getState) => { - const { addresses } = getState().addresses; - const availableAddresses = addresses.filter(a => a.devicePath !== devicePath); + const selected = findSelectedDevice(getState().connect); + + const saved = getState().connect.devices.map(d => { + if (d.checksum) { + return { + instance: d.instance, + checksum: d.checksum + } + } else { + return null; + } + }); + + //const response = await __acquire(selected.path, selected.instance); dispatch({ - type: ACTIONS.ADDRESS_DELETE, - addresses: availableAddresses + type: CONNECT.START_ACQUIRING, + device: selected + }); + + const response = await TrezorConnect.getFeatures({ + device: { + path: selected.path, + } + }); + + const selected2 = findSelectedDevice(getState().connect); + dispatch({ + type: CONNECT.STOP_ACQUIRING, + device: selected2 + }); + + if (response && response.success) { + dispatch({ + type: DEVICE.ACQUIRED, + // checksum: response + }) + } else { + // TODO: handle invalid pin? + console.log("-errror ack", response) + + dispatch({ + type: NOTIFICATION.ADD, + payload: { + type: 'error', + title: 'Acquire device error', + message: response.data.error, + cancelable: true, + actions: [ + { + label: 'Try again', + callback: () => { + dispatch(acquire()) + } + } + ] + } + }) + } + } +} + +export const forgetDevice = (device: any) => { + return (dispatch: any, getState: any): any => { + + // find accounts associated with this device + const accounts: Array = getState().accounts.find(a => a.checksum === device.checksum); + + + // find discovery processes associated with this device + const discovery: Array = getState().discovery.find(d => d.checksum === device.checksum); + + } +} + +// called from Aside - device menu (forget single instance) +export const forget = (device: any) => { + return { + type: CONNECT.FORGET_REQUEST, + device + }; +} + +export const duplicateDevice = (device: any) => { + return async (dispatch: any, getState: any): Promise => { + dispatch({ + type: CONNECT.TRY_TO_DUPLICATE, + device }) } } -export function discover(devicePath): any { +export const onDuplicateDevice = () => { + return async (dispatch: any, getState: any): Promise => { + const selected = findSelectedDevice(getState().connect); + dispatch(onSelectDevice(selected)); + } +} + +export const beginDiscoveryProcess = (device: any, coin: string): any => { return async (dispatch, getState) => { - const { web3 } = getState().web3; + const { config } = getState().localStorage; + const coinToDiscover = config.coins.find(c => c.symbol === coin); + + // TODO: validate device checksum + // const checksum = await __acquire(device.path, device.instance); + // if (checksum && checksum.success) { + // if (checksum.data.xpub !== device.checksum) { + // console.error("Incorrect checksum!"); + // return; + // } + // } + + // acquire and hold session + // get xpub from TREZOR + const response = await TrezorConnect.getPublicKey({ + device: { + path: device.path, + instance: device.instance, + state: device.checksum + }, + path: coinToDiscover.bip44, + confirmation: false, + keepSession: true + }); + + if (!response.success) { + // TODO: check message + console.warn("DISCO ERROR", response) + dispatch({ + type: NOTIFICATION.ADD, + payload: { + type: 'error', + title: 'Discovery error', + message: response.data.error, + cancelable: true, + actions: [ + { + label: 'Try again', + callback: () => { + dispatch(startDiscoveryProcess(device, coin)) + } + } + ] + } + }) + return; + } - const response = await TrezorConnect.getPublicKey({ path: "m/44'/60'/0'/0", confirmation: false }); + // TODO: check for interruption + + // TODO: handle response error const basePath: Array = response.data.path; const hdKey = new HDKey(); hdKey.publicKey = new Buffer(response.data.publicKey, 'hex'); hdKey.chainCode = new Buffer(response.data.chainCode, 'hex'); - const loop = async (index: number) => { - const derivedKey = hdKey.derive(`m/${index}`); - const path = basePath.concat(index); - const ethAddress: string = '0x' + EthereumjsUtil.publicToAddress(derivedKey.publicKey, true).toString('hex'); - const address = new Address(devicePath, index, path, ethAddress); + // send data to reducer + dispatch({ + type: DISCOVERY.START, + coin: coinToDiscover.shortcut, + device, + xpub: response.data.publicKey, + basePath, + hdKey, + }); + + dispatch( startDiscoveryProcess(device, coin) ); + } +} + +export const discoverAddress = (device: any, discoveryProcess: Discovery): any => { + return async (dispatch, getState) => { + + const derivedKey = discoveryProcess.hdKey.derive(`m/${discoveryProcess.accountIndex}`); + const path = discoveryProcess.basePath.concat(discoveryProcess.accountIndex); + const publicAddress: string = EthereumjsUtil.publicToAddress(derivedKey.publicKey, true).toString('hex'); + const ethAddress: string = EthereumjsUtil.toChecksumAddress(publicAddress); + const coin = discoveryProcess.coin; + + dispatch({ + type: ADDRESS.CREATE, + device, + coin, + index: discoveryProcess.accountIndex, + path, + address: ethAddress + }); + + // TODO: check if address was created before + // verify address with TREZOR + const verifyAddress = await TrezorConnect.ethereumGetAddress({ + device: { + path: device.path, + instance: device.instance, + state: device.checksum + }, + address_n: path, + showOnTrezor: false + }); + if (discoveryProcess.interrupted) return; + + if (verifyAddress && verifyAddress.success) { + //const trezorAddress: string = '0x' + verifyAddress.data.message.address; + const trezorAddress: string = EthereumjsUtil.toChecksumAddress(verifyAddress.data.message.address); + if (trezorAddress !== ethAddress) { + // throw inconsistent state error + console.warn("Inconsistent state", trezorAddress, ethAddress); + + dispatch({ + type: NOTIFICATION.ADD, + payload: { + type: 'error', + title: 'Address validation error', + message: `Addresses are different. ${ trezorAddress } : ${ ethAddress }`, + cancelable: true, + actions: [ + { + label: 'Try again', + callback: () => { + dispatch(startDiscoveryProcess(device, discoveryProcess.coin)) + } + } + ] + } + }); + return; + } + } else { + // handle TREZOR communication error dispatch({ - type: ACTIONS.ADDRESS_CREATE, - devicePath, - address - }) + type: NOTIFICATION.ADD, + payload: { + type: 'error', + title: 'Address validation error', + message: verifyAddress.data.error, + cancelable: true, + actions: [ + { + label: 'Try again', + callback: () => { + dispatch(startDiscoveryProcess(device, discoveryProcess.coin)) + } + } + ] + } + }); + return; + } - const balance = await getBalance(ethAddress); + const web3instance = getState().web3.find(w3 => w3.coin === coin); + + const balance = await getBalanceAsync(web3instance.web3, ethAddress); + if (discoveryProcess.interrupted) return; + dispatch({ + type: ADDRESS.SET_BALANCE, + address: ethAddress, + balance: web3instance.web3.fromWei(balance.toString(), 'ether') + }); + + const userTokens = []; + // const userTokens = [ + // { symbol: 'T01', address: '0x58cda554935e4a1f2acbe15f8757400af275e084' }, + // { symbol: 'Lahod', address: '0x3360d0ee34a49d9ac34dce88b000a2903f2806ee' }, + // ]; + for (let i = 0; i < userTokens.length; i++) { + const tokenBalance = await getTokenBalanceAsync(web3instance.erc20, userTokens[i].address, ethAddress); + if (discoveryProcess.interrupted) return; dispatch({ - type: ACTIONS.ADDRESS_SET_BALANCE, - address, - balance: web3.fromWei(balance.toString(), 'ether') + type: TOKEN.SET_BALANCE, + tokenName: userTokens[i].symbol, + ethAddress: ethAddress, + tokenAddress: userTokens[i].address, + balance: tokenBalance.toString() }) + } + + const nonce = await getNonce(web3instance.web3, ethAddress); + if (discoveryProcess.interrupted) return; + dispatch({ + type: ADDRESS.SET_NONCE, + address: ethAddress, + nonce: nonce + }); + + const addressIsEmpty = nonce < 1 && !balance.greaterThan(0); + + if (!addressIsEmpty) { + //dispatch( startDiscoveryProcess(device, discoveryProcess.coin) ); + dispatch( discoverAddress(device, discoveryProcess) ); + } else { + // release acquired sesssion + await TrezorConnect.getPublicKey({ + device: { + path: device.path, + instance: device.instance, + state: device.checksum + }, + path: "m/44'/60'/0'/0", + confirmation: false, + keepSession: false + }); + if (discoveryProcess.interrupted) return; + + dispatch({ + type: DISCOVERY.COMPLETE, + device, + coin + }); + } + } +} + +export function startDiscoveryProcess(device: any, coin: string, ignoreCompleted?: boolean): any { + return (dispatch, getState) => { + + const selected = findSelectedDevice(getState().connect); + if (!selected) { + // TODO: throw error + console.error("Start discovery: no selected device", device) + return; + } else if (selected.path !== device.path) { + console.error("Start discovery: requested device is not selected", device, selected) + return; + } else if (!selected.checksum) { + console.warn("Start discovery: Selected device wasn't authenticated yet...") + return; + } - // const history = await getTransactionHistory(ethAddress); - // dispatch({ - // type: ACTIONS.ADDRESS_SET_HISTORY, - // address, - // history - // }) + const discovery = getState().discovery; + let discoveryProcess: ?Discovery = discovery.find(d => d.checksum === device.checksum && d.coin === coin); - // TODO redirect to 1st account - if (index === 0) { - dispatch( push('/address/0') ); + if (!selected.connected && (!discoveryProcess || !discoveryProcess.completed)) { + dispatch({ + type: DISCOVERY.WAITING, + device, + coin + }); + return; + } + + if (!discoveryProcess) { + dispatch( beginDiscoveryProcess(device, coin) ); + return; + } else { + if (discoveryProcess.completed && !ignoreCompleted) { + dispatch({ + type: DISCOVERY.COMPLETE, + device, + coin + }); + } else if (discoveryProcess.interrupted || discoveryProcess.waitingForDevice) { + // discovery cycle was interrupted + // start from beginning + dispatch( beginDiscoveryProcess(device, coin) ); + } else { + dispatch( discoverAddress(device, discoveryProcess) ); + } + } + } +} + +export const restoreDiscovery = (): any => { + return (dispatch, getState): void => { + const selected = findSelectedDevice(getState().connect); + + if (selected && selected.connected && !selected.unacquired) { + const discoveryProcess: ?Discovery = getState().discovery.find(d => d.checksum === selected.checksum && d.waitingForDevice); + if (discoveryProcess) { + dispatch( startDiscoveryProcess(selected, discoveryProcess.coin) ); } + } + } +} + +// there is no discovery process but it should be +// this is possible race condition when coin was changed in url but device wasn't authenticated yet +// try to discovery after CONNECT.AUTH_DEVICE action +export const checkDiscoveryStatus = (): any => { + return (dispatch, getState): void => { + const selected = findSelectedDevice(getState().connect); + if (!selected) return; - if (index < 2) { - loop( index + 1); + const urlParams = getState().router.location.params; + if (urlParams.coin) { + const discoveryProcess: ?Discovery = getState().discovery.find(d => d.checksum === selected.checksum && d.coin === urlParams.coin); + if (!discoveryProcess) { + dispatch( startDiscoveryProcess(selected, urlParams.coin) ); } } + } +} - loop(0); + + +export function stopDiscoveryProcess(device: any): any { + + // TODO: release devices session + // corner case swtich /eth to /etc (discovery start stop - should not be async) + return { + type: DISCOVERY.STOP, + device } } -export function onSelectDevice(): any { - return async (dispatch, getState) => { - // dispatch(Web3Actions.composeTransaction()); +export function addAddress(): any { + return (dispatch, getState) => { + const selected = findSelectedDevice(getState().connect); + dispatch( startDiscoveryProcess(selected, getState().router.location.params.coin, true) ); // TODO: coin nicer } } diff --git a/src/js/actions/Web3Actions.1.js b/src/js/actions/Web3Actions.1.js deleted file mode 100644 index 23e2a372..00000000 --- a/src/js/actions/Web3Actions.1.js +++ /dev/null @@ -1,154 +0,0 @@ -/* @flow */ -'use strict'; - -import HDKey from 'hdkey'; -import EthereumjsUtil from 'ethereumjs-util'; -import EthereumjsTx from 'ethereumjs-tx'; -import TrezorConnect from 'trezor-connect'; -import { strip } from '../utils/ethUtils'; - -export function getTransaction(web3, txid) { - return new Promise((resolve, reject) => { - web3.eth.getTransaction(txid, (error, result) => { - if (error) { - reject(error); - } else { - resolve(result); - } - }); - }); -} - -export function getBalance(web3, address) { - return new Promise((resolve, reject) => { - web3.eth.getBalance(address, (error, result) => { - if (error) { - reject(error); - } else { - resolve(result); - } - }); - }); -} - -export function getNonce(web3, address) { - return new Promise((resolve, reject) => { - web3.eth.getTransactionCount(address, (error, result) => { - if (error) { - reject(error); - } else { - resolve(result); - } - }); - }); -} - -export function estimateGas(web3, gasOptions) { - return new Promise((resolve, reject) => { - web3.eth.estimateGas(gasOptions, (error, result) => { - if (error) { - reject(error); - } else { - resolve(result); - } - }); - }) -} - -export function getGasPrice(web3) { - return new Promise((resolve, reject) => { - web3.eth.getGasPrice((error, result) => { - if (error) { - reject(error); - } else { - resolve(result); - } - }); - }) -} - -export function push(web3, tx) { - return new Promise((resolve, reject) => { - web3.eth.sendRawTransaction(tx, (error, result) => { - if (error) { - reject(error); - } else { - resolve(result); - } - }); - }) -} - -export function composeTransaction() { - return async function (dispatch, getState) { - const { web3 } = getState().web3; - const { address, amount } = getState().sendForm; - - const resp = await TrezorConnect.getPublicKey({ path: "m/44'/60'/0'/0", confirmation: false }); - - const hdk = new HDKey(); - hdk.publicKey = new Buffer(resp.data.publicKey, 'hex'); - hdk.chainCode = new Buffer(resp.data.chainCode, 'hex'); - - const derivedKey = hdk.derive("m/0"); - const myAddress = EthereumjsUtil.publicToAddress(derivedKey.publicKey, true); - - const txData = { - address_n: [ - (44 | 0x80000000) >>> 0, - (60 | 0x80000000) >>> 0, - (0 | 0x80000000) >>> 0, - 0, 0 - ], - to: address, - value: web3.toHex(web3.toWei(amount, 'ether')), - data, - chainId: 3 - } - - console.log("NONCE", myAddress) - const nonce = await getNonce(web3, '0x' + myAddress.toString('hex') ); - console.log("NONCE", nonce) - - const gasOptions = { - to: txData.to, - data: txData.data - } - const gasLimit = await estimateGas(web3, gasOptions); - const gasPrice = await getGasPrice(web3); - - txData.nonce = web3.toHex(nonce); - txData.gasLimit = web3.toHex(gasLimit); - txData.gasPrice = web3.toHex(gasPrice); - - console.log("NONCE", nonce, gasLimit, gasPrice) - - let signedTransaction = await TrezorConnect.ethereumSignTransaction({ - //path: "m/44'/60'/0'/0/0", - address_n: txData.address_n, - nonce: strip(txData.nonce), - gas_price: strip(txData.gasPrice), - gas_limit: strip(txData.gasLimit), - to: strip(txData.to), - value: strip(txData.value), - data: txData.data, - chain_id: txData.chainId - }); - - txData.r = '0x' + signedTransaction.data.r; - txData.s = '0x' + signedTransaction.data.s; - txData.v = web3.toHex(signedTransaction.data.v); - - const tx = new EthereumjsTx(txData); - const serializedTx = '0x' + tx.serialize().toString('hex'); - - const txid = await push(web3, serializedTx); - - dispatch({ - type: 'tx_complete', - txid - }) - - console.log("TXID", txid); - } -} \ No newline at end of file diff --git a/src/js/actions/Web3Actions.js b/src/js/actions/Web3Actions.js index 14e803fc..dd98876f 100644 --- a/src/js/actions/Web3Actions.js +++ b/src/js/actions/Web3Actions.js @@ -1,37 +1,270 @@ /* @flow */ 'use strict'; +import Web3 from 'web3'; import HDKey from 'hdkey'; import EthereumjsUtil from 'ethereumjs-util'; import EthereumjsTx from 'ethereumjs-tx'; import TrezorConnect from 'trezor-connect'; import { strip } from '../utils/ethUtils'; import * as ACTIONS from './index'; +import * as ADDRESS from './constants/Address'; +import * as WEB3 from './constants/Web3'; +import { loadHistory } from '../services/EtherscanService'; +import { httpRequest } from '../utils/networkUtils'; +type ActionMethod = (dispatch: any, getState: any) => Promise; -export function getBalance(address) { +export function init(web3: ?Web3, coinIndex: number = 0): ActionMethod { return async (dispatch, getState) => { - const { web } = getState().web3; - web3.eth.getBalance(address.address, (error, balance) => { - if (!error) { + + const { config, ethERC20 } = getState().localStorage; + + const coin = config.coins[ coinIndex ]; + if (!coin) { + // all instances done + dispatch({ + type: WEB3.READY, + }); + return; + } + + const coinName = coin.shortcut; + const urls = coin.backends[0].urls; + + let web3host: string = urls[0]; + + if (web3) { + const currentHost = web3.currentProvider.host; + let currentHostIndex: number = urls.indexOf(currentHost); + + if (currentHostIndex + 1 < urls.length) { + web3host = urls[currentHostIndex + 1]; + } else { + console.error("TODO: Backend " + coinName + " not working"); + // try next coin + dispatch( init(web3, coinIndex + 1) ); + return; + } + } + + //const instance = new Web3(window.web3.currentProvider); + const instance = new Web3( new Web3.providers.HttpProvider(web3host) ); + + // instance = new Web3( new Web3.providers.HttpProvider('https://pyrus2.ubiqscan.io') ); // UBQ + //instance = new Web3( new Web3.providers.HttpProvider('https://node.expanse.tech/') ); // EXP + //instance = new Web3( new Web3.providers.HttpProvider('http://10.34.0.91:8545/') ); + + //web3 = new Web3(new Web3.providers.HttpProvider("https://api.myetherapi.com/rop")); + //instance = new Web3(new Web3.providers.HttpProvider("https://ropsten.infura.io2/QGyVKozSUEh2YhL4s2G4")); + //web3 = new Web3( new Web3.providers.HttpProvider("ws://34.230.234.51:30303") ); + + // initial check if backend is running + // instance.version.getNetwork(function(error, chainId){ + // if (!error) { + + + + instance.eth.getGasPrice((error, gasPrice) => { + if (error) { + // try different url + dispatch( init(instance, coinIndex) ); + } else { + + const erc20 = instance.eth.contract(ethERC20); + dispatch({ - type: ACTIONS.ADDRESS_SET_BALANCE, - address, - balance: web3.fromWei(balance.toString(), 'ether') - }) + type: WEB3.CREATE, + name: coinName, + web3: instance, + erc20, + chainId: instance.version.network + }); + + dispatch({ + type: WEB3.GAS_PRICE_UPDATED, + coin: coinName, + gasPrice + }); + + + + + // console.log("GET CHAIN", instance.version.network) + + // instance.version.getWhisper((err, shh) => { + // console.log("-----whisperrr", error, shh) + // }) + + + // const sshFilter = instance.ssh.filter('latest'); + // sshFilter.watch((error, blockHash) => { + // console.warn("SSH", error, blockHash); + // }); + + //const shh = instance.shh.newIdentity(); + + const latestBlockFilter = instance.eth.filter('latest'); + latestBlockFilter.watch(async (error, blockHash) => { + + if (error) { + console.warn("ERROR!", error); + + // setInterval(() => { + // dispatch( getGasPrice(coinName) ); + // }, 5000); + } + + dispatch({ + type: WEB3.BLOCK_UPDATED, + name: coinName, + blockHash + }); + + // TODO: filter only current device + const accounts = getState().accounts.filter(a => a.coin === coinName); + for (const addr of accounts) { + dispatch( getBalance(addr) ); + } + + dispatch( getGasPrice(coinName) ); + + // if (pendingTxs.length > 0) { + // for (const tx of pendingTxs) { + // dispatch( getTransactionReceipt(tx) ); + // } + // } + }); + + // init next coin + dispatch( init(instance, coinIndex + 1) ); + } }); + + // let instance2 = new Web3( new Web3.providers.HttpProvider('https://pyrus2.ubiqscan.io') ); + // console.log("INIT WEB3", instance, instance2); + // instance2.eth.getGasPrice((error, gasPrice) => { + // console.log("---gasss price from UBQ", gasPrice) + // }); + } +} + +function initBlockTicker() { + +} + +export function initContracts(): ActionMethod { + return async (dispatch, getState) => { + const { web3, abi, tokens } = getState().web3; + + const contracts = []; + for (let token of tokens) { + contracts.push({ + contract: web3.eth.contract(abi).at(token.address), + name: token.name, + symbol: token.symbol, + decimal: token.decimal + }); + + // web3.eth.contract(abi).at(token.address).balanceOf('0x98ead4bd2fbbb0cf0b49459aa0510ef53faa6cad', (e, r) => { + // console.warn('contrR', e, r.toString(10)); + // }); + } + + const contract = web3.eth.contract(abi).at('0x58cda554935e4a1f2acbe15f8757400af275e084'); + + contract.name.call((error, name) => { + if (error) { + // TODO: skip + } + contract.symbol.call((error, symbol) => { + if (error) { + // TODO: skip + } + + contract.decimals.call((error, decimals) => { + console.log("nameeeee", name, symbol, decimals) + }) + }); + + + }) } } +export function getGasPrice(coinName: string): ActionMethod { + return async (dispatch, getState) => { + const index: number = getState().web3.findIndex(w3 => { + return w3.coin === coinName; + }); + const web3 = getState().web3[ index ].web3; + web3.eth.getGasPrice((error, gasPrice) => { + if (!error) { + dispatch({ + type: WEB3.GAS_PRICE_UPDATED, + coin: coinName, + gasPrice + }); + } + }); + } +} +export function getBalance(addr: Address): ActionMethod { + return async (dispatch, getState) => { + const web3instance = getState().web3.filter(w3 => w3.coin === addr.coin)[0]; + const web3 = web3instance.web3; + web3.eth.getBalance(addr.address, (error, balance) => { + if (!error) { + const newBalance: string = web3.fromWei(balance.toString(), 'ether'); + if (addr.balance !== newBalance) { + dispatch({ + type: ADDRESS.SET_BALANCE, + address: addr.address, + balance: newBalance + }); + + // dispatch( loadHistory(addr) ); + } + } + }); + } +} +export function getTransactionReceipt(txid: string): any { + return async (dispatch, getState) => { + const { web3 } = getState().web3; + //web3.eth.getTransactionReceipt(txid, (error, tx) => { + web3.eth.getTransaction(txid, (error, tx) => { + if (tx && tx.blockNumber) { + web3.eth.getBlock(tx.blockHash, (error, block) => { + console.log("---MAMM BLOCK", error, block, tx, tx.blockHash) + dispatch({ + type: ACTIONS.TX_CONFIRMED, + txid, + tx, + block + }) + }); + } + }); + } +} + + +export function updateLastBlock(hash: string) { + return { + type: 'web3__update_last_block', + hash + } +} export function getTransaction(web3, txid) { return new Promise((resolve, reject) => { @@ -45,7 +278,9 @@ export function getTransaction(web3, txid) { }); } -export function getBalance2(web3, address) { + + +export function getBalanceAsync(web3, address) { return new Promise((resolve, reject) => { web3.eth.getBalance(address, (error, result) => { if (error) { @@ -57,6 +292,20 @@ export function getBalance2(web3, address) { }); } +export const getTokenBalanceAsync = (erc20: any, token: any, address: any): Promise => { + return new Promise((resolve, reject) => { + + const contr = erc20.at(token); + contr.balanceOf(address, (error, result) => { + if (error) { + reject(error); + } else { + resolve(result); + } + }); + }); +} + export function getNonce(web3, address) { return new Promise((resolve, reject) => { web3.eth.getTransactionCount(address, (error, result) => { @@ -69,6 +318,41 @@ export function getNonce(web3, address) { }); } + +export function getTokenInfoAsync(erc20: any, address: string): Promise { + return new Promise((resolve, reject) => { + + const contract = erc20.at(address); + const info = {}; + // TODO: handle errors + contract.name.call((e, name) => { + if (e) { + //console.log("1", address, e) + //resolve(null); + //return; + } + info.name = name; + contract.symbol.call((e, symbol) => { + if (e) { + console.log("2", e) + resolve(null); + return; + } + info.symbol = symbol; + contract.decimals.call((e, decimals) => { + if (e) { + console.log("3", e) + resolve(null); + return; + } + info.decimals = decimals.toString(); + resolve(info); + }); + }) + }); + }); +} + export function estimateGas(web3, gasOptions) { return new Promise((resolve, reject) => { web3.eth.estimateGas(gasOptions, (error, result) => { @@ -81,7 +365,7 @@ export function estimateGas(web3, gasOptions) { }) } -export function getGasPrice(web3) { +export function getGasPrice2(web3) { return new Promise((resolve, reject) => { web3.eth.getGasPrice((error, result) => { if (error) { @@ -93,7 +377,7 @@ export function getGasPrice(web3) { }) } -export function push(web3, tx) { +export function pushTx(web3, tx) { return new Promise((resolve, reject) => { web3.eth.sendRawTransaction(tx, (error, result) => { if (error) { @@ -168,7 +452,7 @@ export function composeTransaction() { const tx = new EthereumjsTx(txData); const serializedTx = '0x' + tx.serialize().toString('hex'); - const txid = await push(web3, serializedTx); + const txid = await pushTx(web3, serializedTx); dispatch({ type: 'tx_complete', @@ -177,4 +461,9 @@ export function composeTransaction() { console.log("TXID", txid); } -} \ No newline at end of file +} + + + + + diff --git a/src/js/actions/constants/Discovery.js b/src/js/actions/constants/Discovery.js new file mode 100644 index 00000000..152f43d3 --- /dev/null +++ b/src/js/actions/constants/Discovery.js @@ -0,0 +1,8 @@ +/* @flow */ +'use strict'; + +export const START: string = 'discovery__start'; +export const STOP: string = 'discovery__stop'; +export const COMPLETE: string = 'discovery__complete'; +export const WAITING: string = 'discovery__waiting'; +export const FROM_STORAGE: string = 'discovery__from_storage'; \ No newline at end of file diff --git a/src/js/actions/constants/LocalStorage.js b/src/js/actions/constants/LocalStorage.js new file mode 100644 index 00000000..479d9672 --- /dev/null +++ b/src/js/actions/constants/LocalStorage.js @@ -0,0 +1,6 @@ +/* @flow */ +'use strict'; + +export const SAVE: string = 'storage__save'; +export const READY: string = 'storage__ready'; +export const ERROR: string = 'storage__error'; \ No newline at end of file diff --git a/src/js/actions/constants/Modal.js b/src/js/actions/constants/Modal.js new file mode 100644 index 00000000..f906b7b5 --- /dev/null +++ b/src/js/actions/constants/Modal.js @@ -0,0 +1,13 @@ +export const ON_PASSPHRASE_CHANGE: string = 'action__on_passphrase_change'; +export const ON_PASSPHRASE_SHOW: string = 'action__on_passphrase_show'; +export const ON_PASSPHRASE_HIDE: string = 'action__on_passphrase_hide'; +export const ON_PASSPHRASE_SAVE: string = 'action__on_passphrase_save'; +export const ON_PASSPHRASE_FORGET: string = 'action__on_passphrase_forget'; +export const ON_PASSPHRASE_FOCUS: string = 'action__on_passphrase_focus'; +export const ON_PASSPHRASE_BLUR: string = 'action__on_passphrase_blur'; +export const ON_PASSPHRASE_SUBMIT: string = 'action__on_passphrase_submit'; + +export const FORGET: string = 'modal__forget'; +export const REMEMBER: string = 'modal__remember'; +export const ON_FORGET: string = 'modal__on_forget'; +export const ON_REMEMBER: string = 'modal__on_remember'; diff --git a/src/js/actions/constants/SendForm.js b/src/js/actions/constants/SendForm.js new file mode 100644 index 00000000..7e33336b --- /dev/null +++ b/src/js/actions/constants/SendForm.js @@ -0,0 +1,19 @@ +/* @flow */ +'use strict'; + +export const INIT: string = 'send__init'; +export const DISPOSE: string = 'send__dispose'; +export const VALIDATION: string = 'send__validation'; +export const ADDRESS_CHANGE: string = 'send__address_change'; +export const AMOUNT_CHANGE: string = 'send__amount_change'; +export const SET_MAX: string = 'send__set_max'; +export const CURRENCY_CHANGE: string = 'send__currency_change'; +export const FEE_LEVEL_CHANGE: string = 'send__fee_level_change'; +export const GAS_PRICE_CHANGE: string = 'send__gas_price_change'; +export const GAS_LIMIT_CHANGE: string = 'send__gas_limit_change'; +export const UPDATE_FEE_LEVELS: string = 'send__update_fee_levels'; +export const DATA_CHANGE: string = 'send__data_change'; +export const SEND: string = 'send__submit'; +export const TX_COMPLETE: string = 'send__tx_complete'; +export const TX_ERROR: string = 'send__tx_error'; +export const TOGGLE_ADVANCED: string = 'send__toggle_advanced'; \ No newline at end of file diff --git a/src/js/actions/constants/Token.js b/src/js/actions/constants/Token.js new file mode 100644 index 00000000..ed88dc03 --- /dev/null +++ b/src/js/actions/constants/Token.js @@ -0,0 +1,7 @@ +/* @flow */ +'use strict'; + +export const ADD: string = 'token__add'; +export const REMOVE: string = 'token__remove'; +export const SET_BALANCE: string = 'token__set_balance'; +export const FROM_STORAGE: string = 'token__from_storage'; \ No newline at end of file diff --git a/src/js/actions/constants/TrezorConnect.js b/src/js/actions/constants/TrezorConnect.js new file mode 100644 index 00000000..06b0a4b4 --- /dev/null +++ b/src/js/actions/constants/TrezorConnect.js @@ -0,0 +1,26 @@ +/* @flow */ +'use strict'; + +export const READY: string = 'trezorconnect__ready'; +export const INITIALIZATION_ERROR: string = 'trezorconnect__init_error'; +export const SELECT_DEVICE: string = 'trezorconnect__select_device'; + + +export const DEVICE_FROM_STORAGE: string = 'trezorconnect__device_from_storage'; +export const AUTH_DEVICE: string = 'trezorconnect__auth_device'; +export const COIN_CHANGED: string = 'trezorconnect__coin_changed'; + +export const REMEMBER_REQUEST: string = 'trezorconnect__remember_request'; +export const FORGET_REQUEST: string = 'trezorconnect__forget_request'; +export const FORGET: string = 'trezorconnect__forget'; +export const FORGET_SINGLE: string = 'trezorconnect__forget_single'; +export const DISCONNECT_REQUEST: string = 'trezorconnect__disconnect_request'; +export const REMEMBER: string = 'trezorconnect__remember'; + +export const START_ACQUIRING: string = 'trezorconnect__start_acquiring'; +export const STOP_ACQUIRING: string = 'trezorconnect__stop_acquiring'; + +export const TRY_TO_DUPLICATE: string = 'trezorconnect__try_to_duplicate'; +export const DUPLICATE: string = 'trezorconnect__duplicate'; + +export const DEVICE_STATE_EXCEPTION: string = 'trezorconnect__device_state_exception'; \ No newline at end of file diff --git a/src/js/actions/constants/Web3.js b/src/js/actions/constants/Web3.js new file mode 100644 index 00000000..6334e1a0 --- /dev/null +++ b/src/js/actions/constants/Web3.js @@ -0,0 +1,9 @@ +/* @flow */ +'use strict'; + +export const START: string = 'web3__start'; +export const STOP: string = 'web3__stop'; +export const CREATE: string = 'web3__create'; +export const READY: string = 'web3__ready'; +export const BLOCK_UPDATED: string = 'web3__block_updated'; +export const GAS_PRICE_UPDATED: string = 'web3__gas_price_updated'; \ No newline at end of file diff --git a/src/js/actions/constants/account.js b/src/js/actions/constants/account.js new file mode 100644 index 00000000..d6bcb858 --- /dev/null +++ b/src/js/actions/constants/account.js @@ -0,0 +1,11 @@ +/* @flow */ +'use strict'; + +export const INIT: string = 'account__init'; +export const DISPOSE: string = 'account__dispose'; + +export const CREATE: string = 'address__create'; +export const REMOVE: string = 'address__remove'; +export const SET_BALANCE: string = 'address__set_balance'; +export const SET_NONCE: string = 'address__set_nonce'; +export const FROM_STORAGE: string = 'address__from_storage'; \ No newline at end of file diff --git a/src/js/actions/constants/address.js b/src/js/actions/constants/address.js new file mode 100644 index 00000000..23e87cd0 --- /dev/null +++ b/src/js/actions/constants/address.js @@ -0,0 +1,9 @@ +/* @flow */ +'use strict'; + +export const CREATE: string = 'address__create'; +export const REMOVE: string = 'address__remove'; +export const SET_BALANCE: string = 'address__set_balance'; +export const SET_NONCE: string = 'address__set_nonce'; +export const FROM_STORAGE: string = 'address__from_storage'; + diff --git a/src/js/actions/constants/notification.js b/src/js/actions/constants/notification.js new file mode 100644 index 00000000..d1acbcc5 --- /dev/null +++ b/src/js/actions/constants/notification.js @@ -0,0 +1,6 @@ +/* @flow */ +'use strict'; + +export const ADD: string = 'notification__add'; +export const CLOSE: string = 'notification__close'; +export const REMOVE: string = 'account__remove'; \ No newline at end of file diff --git a/src/js/actions/constants/receive.js b/src/js/actions/constants/receive.js new file mode 100644 index 00000000..49c0f5cd --- /dev/null +++ b/src/js/actions/constants/receive.js @@ -0,0 +1,8 @@ +/* @flow */ +'use strict'; + +export const INIT: string = 'receive__init'; +export const DISPOSE: string = 'receive__dispose'; +export const REQUEST_UNVERIFIED: string = 'receive__request_unverified'; +export const SHOW_ADDRESS: string = 'receive__show_address'; +export const SHOW_UNVERIFIED_ADDRESS: string = 'receive__show_unverified'; diff --git a/src/js/actions/constants/summary.js b/src/js/actions/constants/summary.js new file mode 100644 index 00000000..02dae814 --- /dev/null +++ b/src/js/actions/constants/summary.js @@ -0,0 +1,7 @@ +/* @flow */ +'use strict'; + +export const INIT: string = 'summary__init'; +export const DISPOSE: string = 'summary__dispose'; +export const ADD_TOKEN: string = 'summary__add_token'; +export const DETAILS_TOGGLE: string = 'summary__details_toggle'; diff --git a/src/js/actions/index.js b/src/js/actions/index.js index f5e150b6..45a72ee2 100644 --- a/src/js/actions/index.js +++ b/src/js/actions/index.js @@ -3,41 +3,44 @@ export const CLOSE_MODAL: string = 'action__close_modal'; -export const ON_PIN_ADD: string = 'action__on_pin_click'; -export const ON_PIN_BACKSPACE: string = 'action__on_pin_backspace'; export const ON_PIN_SUBMIT: string = 'action__on_pin_submit'; -export const ON_PASSPHRASE_CHANGE: string = 'action__on_passphrase_change'; -export const ON_PASSPHRASE_SHOW: string = 'action__on_passphrase_show'; -export const ON_PASSPHRASE_HIDE: string = 'action__on_passphrase_hide'; -export const ON_PASSPHRASE_SAVE: string = 'action__on_passphrase_save'; -export const ON_PASSPHRASE_FORGET: string = 'action__on_passphrase_forget'; -export const ON_PASSPHRASE_FOCUS: string = 'action__on_passphrase_focus'; -export const ON_PASSPHRASE_BLUR: string = 'action__on_passphrase_blur'; export const ON_PASSPHRASE_SUBMIT: string = 'action__on_passphrase_submit'; -export const ON_CHANGE_ACCOUNT: string = 'action__on_change_account'; -export const ON_CUSTOM_FEE_OPEN: string = 'action__on_custom_fee_open'; -export const ON_CUSTOM_FEE_CHANGE: string = 'action__on_custom_fee_change'; - -export const ON_SELECT_DEVICE: string = 'action__on_select_device'; - export const ON_ADDRESS_CHANGE: string = 'send__on_address_change'; export const ON_AMOUNT_CHANGE: string = 'send__on_amount_change'; +export const ON_FEE_LEVEL_CHANGE: string = 'send__on_fee_level_change'; export const ON_GAS_PRICE_CHANGE: string = 'send__on_gas_price_change'; export const ON_GAS_LIMIT_CHANGE: string = 'send__on_gas_limit_change'; export const ON_TX_DATA_CHANGE: string = 'send__on_data_change'; export const ON_TX_SEND: string = 'send__on_send'; export const ON_TX_COMPLETE: string = 'send__on_tx_complete'; +export const ON_GAS_PRICE_UPDATE: string = 'send__on_gas_price_update'; export const ADDRESS_CREATE: string = 'address__create'; export const ADDRESS_DELETE: string = 'address__delete'; -export const ADDRESS_SET_BALANCE: string = 'address__set_balance'; +export const ADDRESS_SET_BALANCE: string = 'address2__set_balance'; export const ADDRESS_SET_HISTORY: string = 'address__set_history'; export const ADDRESS_UPDATE_BALANCE: string = 'address__update_balance'; +export const ADDRESS_ADD_TO_HISTORY: string = 'address__add_to_history'; export const TX_STATUS_OK: string = 'tx__status_ok'; export const TX_STATUS_ERROR: string = 'tx__status_error'; -export const TX_STATUS_UNKNOWN: string = 'tx__status_unknown'; \ No newline at end of file +export const TX_STATUS_UNKNOWN: string = 'tx__status_unknown'; +export const TX_CONFIRMED: string = 'tx__confirmed'; + + + +export const TOKENS_TOGGLE_SUMMARY: string = 'tokens_toggle_summary'; + +export const TOKENS_SEARCH: string = 'tokens_search'; + + +export const TOKENS_CUSTOM_TOGGLE: string = 'tokens_custom_toggle'; +export const TOKENS_CUSTOM_ADDRESS_CHANGE: string = 'tokens_custom_address_change'; +export const TOKENS_CUSTOM_NAME_CHANGE: string = 'tokens_custom_name_change'; +export const TOKENS_CUSTOM_SHORTCUT_CHANGE: string = 'tokens_custom_shortcut_change'; +export const TOKENS_CUSTOM_DECIMAL_CHANGE: string = 'tokens_custom_decimal_change'; +export const TOKENS_CUSTOM_ADD: string = 'tokens_custom_add'; \ No newline at end of file diff --git a/src/js/components/AddressMenu.js b/src/js/components/AddressMenu.js deleted file mode 100644 index ed9caa28..00000000 --- a/src/js/components/AddressMenu.js +++ /dev/null @@ -1,27 +0,0 @@ -/* @flow */ -'use strict'; - -import React from 'react'; -import { NavLink } from 'react-router-dom'; - -const AddressMenu = (props): any => { - - const { addresses } = props.addresses; - - let accounts = addresses.map((address, i) => { - return ( - - { `Address #${(address.index + 1 )}` } - { address.balance } ETH - - ) - }) - - return ( -
- { accounts } -
- ); -} - -export default AddressMenu; \ No newline at end of file diff --git a/src/js/components/AddressTab.js b/src/js/components/AddressTab.js deleted file mode 100644 index c6b02e94..00000000 --- a/src/js/components/AddressTab.js +++ /dev/null @@ -1,27 +0,0 @@ -/* @flow */ -'use strict'; - -import React from 'react'; -import { Link } from 'react-router-dom'; - -const AddressTab = (props): any => { - - const urlParams = props.match.params; - const basePath = `/address/${urlParams.address}`; - - return ( -
- - History - - - Send - - - Receive - -
- ); -} - -export default AddressTab; \ No newline at end of file diff --git a/src/js/components/Devices.js b/src/js/components/Devices.js deleted file mode 100644 index a0f493c6..00000000 --- a/src/js/components/Devices.js +++ /dev/null @@ -1,42 +0,0 @@ -/* @flow */ -'use strict'; - -import React, { Component } from 'react'; - -export default class Devices extends Component { - render() { - const { devices, selectedDevice } = this.props.connect; - const deviceList: Array = devices.map((dev, index) => { - let css: string = ""; - if (dev.unacquired) { - css += "unacquired"; - } - if (dev.isUsedElsewhere) { - css += " used-elsewhere"; - } - if (dev.featuresNeedsReload) { - css += " reload-features"; - } - if (dev.path === selectedDevice) { - css += " active"; - } - return (
  • this.props.onSelectDevice(dev.path) } >{ dev.label }
  • ); - }); - - if (deviceList.length === 0) { - deviceList.push( - (
  • No connected devices
  • ) - ); - } - - return ( - - ); - } -} diff --git a/src/js/components/Footer.js b/src/js/components/Footer.js deleted file mode 100644 index c453ff17..00000000 --- a/src/js/components/Footer.js +++ /dev/null @@ -1,17 +0,0 @@ -/* @flow */ -'use strict'; - -import React, { Component } from 'react'; - -export default class Footer extends Component { - render() { - return ( - - ); - } -} diff --git a/src/js/components/Main.js b/src/js/components/Main.js deleted file mode 100644 index e8be715f..00000000 --- a/src/js/components/Main.js +++ /dev/null @@ -1,20 +0,0 @@ -/* @flow */ -'use strict'; - -import React, { Component } from 'react'; - -export default class Main extends Component { - render() { - return ( -
    -
    -
      -
    -
    -
    - { this.props.children } -
    -
    - ); - } -} diff --git a/src/js/components/Receive.js b/src/js/components/Receive.js deleted file mode 100644 index ee4bd1d9..00000000 --- a/src/js/components/Receive.js +++ /dev/null @@ -1,30 +0,0 @@ -/* @flow */ -'use strict'; - -import React, { Component } from 'react'; -import AddressTab from './AddressTab'; -import { QRCode } from 'react-qr-svg'; - -const History = (props): any => { - - const { addresses } = props.addresses; - const currentAddress = addresses[ parseInt(props.match.params.address) ]; - - if (!currentAddress) return null; - - return ( -
    - -

    { currentAddress.address }

    - -
    - ); -} - -export default History; diff --git a/src/js/components/SendForm.js b/src/js/components/SendForm.js deleted file mode 100644 index 876fe49e..00000000 --- a/src/js/components/SendForm.js +++ /dev/null @@ -1,72 +0,0 @@ -/* @flow */ -'use strict'; - -import React, { Component } from 'react'; -import AddressTab from './AddressTab'; - -const SendForm = (props): any => { - - console.log("ENDFORM", props) - - const addressId = parseInt( props.match.params.address ); - - const { - address, - amount, - gasPrice, - gasLimit, - data - } = props.sendForm; - - const { - onAddressChange, - onAmountChange, - onGasPriceChange, - onGasLimitChange, - onDataChange, - onSend - } = props.sendFormActions; - - const disabled = false; - - return ( -
    - - - -
    - - onAddressChange(event.target.value) } /> -
    - -
    - - onAmountChange(event.target.value) } /> -
    - -
    - - onGasLimitChange(event.target.value) } /> -
    - -
    - - onGasPriceChange(event.target.value) } /> - GWEI -
    - -
    - - onDataChange(event.target.value) } /> -
    - -
    - - -
    - -
    - ); -} - -export default SendForm; diff --git a/src/js/components/common/Footer.js b/src/js/components/common/Footer.js new file mode 100644 index 00000000..d69f8dc7 --- /dev/null +++ b/src/js/components/common/Footer.js @@ -0,0 +1,17 @@ +/* @flow */ +'use strict'; + +import React from 'react'; + +const Footer = (props: any): any => { + return ( + + ); +} + +export default Footer; diff --git a/src/js/components/Header.js b/src/js/components/common/Header.js similarity index 97% rename from src/js/components/Header.js rename to src/js/components/common/Header.js index c898acca..7ffe5b0b 100644 --- a/src/js/components/Header.js +++ b/src/js/components/common/Header.js @@ -9,7 +9,6 @@ export default class Header extends Component {
    - TrezorConnect
    ); diff --git a/src/js/components/common/LoaderCircle.js b/src/js/components/common/LoaderCircle.js new file mode 100644 index 00000000..202eda91 --- /dev/null +++ b/src/js/components/common/LoaderCircle.js @@ -0,0 +1,22 @@ +/* @flow */ +'use strict'; + +import React from 'react'; + +export default (props: any): any => { + + const style = { + width: `${props.size}px`, + height: `${props.size}px`, + } + + return ( +
    +

    { props.label }

    + + + + +
    + ); +} \ No newline at end of file diff --git a/src/js/components/common/Log.js b/src/js/components/common/Log.js new file mode 100644 index 00000000..62ac4e18 --- /dev/null +++ b/src/js/components/common/Log.js @@ -0,0 +1,40 @@ +/* @flow */ +'use strict'; + +import React from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; + +import * as SendFormActions from '../../actions/SendFormActions'; +import { getAddress } from '../../actions/TrezorConnectActions'; + + +const Log = (props: any) => { + return ( +
    + Log +
    + ) +} + +function mapStateToProps(state, own) { + +} + +function mapDispatchToProps(dispatch) { + +} + +export default connect( + (state) => { + return { + accounts: state.accounts, + receive: state.receive + }; + }, + (dispatch) => { + return { + getAddress: bindActionCreators(getAddress, dispatch), + }; + } +)(Log); \ No newline at end of file diff --git a/src/js/components/common/Notification.js b/src/js/components/common/Notification.js new file mode 100644 index 00000000..21ecbff5 --- /dev/null +++ b/src/js/components/common/Notification.js @@ -0,0 +1,74 @@ +/* @flow */ +'use strict'; + +import React from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; + +import * as NOTIFICATION from '../../actions/constants/notification'; + + +export const Notification = (props: any) => { + const className = `notification ${ props.className }`; + + + const actionButtons = !props.actions ? null : props.actions.map((a, i) => { + return ( + + ) + }); + + return ( +
    + { props.cancelable ? ( + + ) : null } +
    +

    { props.title }

    +

    +
    + { props.actions && props.actions.length > 0 ? ( +
    + { actionButtons } +
    + ) : null } + +
    + ) +} + +export const NotificationGroup = (props: any) => { + const { notifications, close } = props; + return notifications.map((n, i) => { + return ( + + ) + }); +} + +export default connect( + (state) => { + return { + notifications: state.notifications + }; + }, + (dispatch) => { + return { + close: bindActionCreators((notif) => { + return { + type: NOTIFICATION.CLOSE, + payload: notif + } + }, dispatch), + }; + } +)(NotificationGroup); \ No newline at end of file diff --git a/src/js/components/landing/ConnectDevice.js b/src/js/components/landing/ConnectDevice.js new file mode 100644 index 00000000..32f25e01 --- /dev/null +++ b/src/js/components/landing/ConnectDevice.js @@ -0,0 +1,36 @@ +/* @flow */ +'use strict'; + +import React from 'react'; +import Header from '../common/Header'; +import Footer from '../common/Footer'; + +export default (props: any): any => { + return ( +
    +
    +
    +

    The private bank in your hands.

    +

    TREZOR Wallet is an easy-to-use interface for your TREZOR.

    +

    TREZOR Wallet allows you to easily control your funds, manage your balance and initiate transfers.

    +
    +

    + + + + + + + + + Connect TREZOR to continue +

    + {/*

    Don't have TREZOR? Get one

    */} +
    +
    +

    Don't have TREZOR? Get one

    +
    +
    +
    + ); +} \ No newline at end of file diff --git a/src/js/components/landing/LandingPage.js b/src/js/components/landing/LandingPage.js new file mode 100644 index 00000000..440ba3f8 --- /dev/null +++ b/src/js/components/landing/LandingPage.js @@ -0,0 +1,73 @@ +/* @flow */ +'use strict'; + +import React from 'react'; +import Preloader from './Preloader'; +import ConnectDevice from './ConnectDevice'; +import LocalStorageError from './LocalStorageError'; +import TrezorConnectError from './TrezorConnectError'; +import Header from '../common/Header'; +import Footer from '../common/Footer'; +import { Notification } from '../common/Notification'; + +export default (props: any): any => { + + const web3 = props.web3; + const { devices } = props.connect; + const localStorageError = props.localStorage.error; + const connectError = props.connect.error; + + let notification = null; + + if (localStorageError) { + notification = (); + } + + if (connectError) { + notification = (); + } + + if (notification || (web3.length > 0 && devices.length < 1)) { + return ( +
    +
    + { notification } +
    +

    The private bank in your hands.

    +

    TREZOR Wallet is an easy-to-use interface for your TREZOR.

    +

    TREZOR Wallet allows you to easily control your funds, manage your balance and initiate transfers.

    +
    +

    + + + + + + + + + + Connect TREZOR to continue + +

    + {/* */} + {/*

    Don't have TREZOR? Get one

    */} +
    +
    +

    Don't have TREZOR? Get one

    +
    +
    +
    + ); + } else { + return (); + } +} diff --git a/src/js/components/landing/LocalStorageError.js b/src/js/components/landing/LocalStorageError.js new file mode 100644 index 00000000..1e61c062 --- /dev/null +++ b/src/js/components/landing/LocalStorageError.js @@ -0,0 +1,12 @@ +/* @flow */ +'use strict'; + +import React from 'react'; + +export default (props: any): any => { + return ( +
    + localstorage ERROR +
    + ); +} \ No newline at end of file diff --git a/src/js/components/landing/Preloader.js b/src/js/components/landing/Preloader.js new file mode 100644 index 00000000..85925faf --- /dev/null +++ b/src/js/components/landing/Preloader.js @@ -0,0 +1,13 @@ +/* @flow */ +'use strict'; + +import React from 'react'; +import Loader from '../common/LoaderCircle'; + +export default (props: any): any => { + return ( +
    + +
    + ); +} \ No newline at end of file diff --git a/src/js/components/landing/TrezorConnectError.js b/src/js/components/landing/TrezorConnectError.js new file mode 100644 index 00000000..cd3c9e51 --- /dev/null +++ b/src/js/components/landing/TrezorConnectError.js @@ -0,0 +1,12 @@ +/* @flow */ +'use strict'; + +import React from 'react'; + +export default (props: any): any => { + return ( +
    + connect ERROR +
    + ); +} \ No newline at end of file diff --git a/src/js/components/modal/AccountSelection.js b/src/js/components/modal/AccountSelection.js deleted file mode 100644 index 5ef6cb5e..00000000 --- a/src/js/components/modal/AccountSelection.js +++ /dev/null @@ -1,41 +0,0 @@ -/* @flow */ -'use strict'; - -import React from 'react'; -import { formatAmount } from '../../utils/formatUtils'; - -const AccountSelection = (props): any => { - - const { accounts, coinInfo, complete } = props.modal; - const accountsCollection = accounts.map((a, index) => { - - let accountStatus: string = a.fresh ? 'Fresh account' : formatAmount(a.balance, coinInfo); - // Loading... - - return ( -
    - -
    - ) - }); - - const header: string = complete ? `Select ${ coinInfo.label } account` : `Loading ${ coinInfo.label } accounts...`; - - return ( -
    -

    { header }

    -
    -
    Accounts
    -
    Legacy Accounts
    -
    -
    - { accountsCollection } -
    -
    - ); -} - -export default AccountSelection; \ No newline at end of file diff --git a/src/js/components/modal/ConfirmAddress.js b/src/js/components/modal/ConfirmAddress.js new file mode 100644 index 00000000..7af6ddf3 --- /dev/null +++ b/src/js/components/modal/ConfirmAddress.js @@ -0,0 +1,55 @@ +/* @flow */ +'use strict'; + +import React from 'react'; +import { findSelectedDevice } from '../../reducers/TrezorConnectReducer'; + +const ConfirmAddress = (props: any): any => { + + const account = props.accounts.find(a => a.checksum === props.receive.checksum && a.index === props.receive.accountIndex && a.coin === props.receive.coin); + + return ( +
    +
    +

    Confirm address on TREZOR

    +

    Please compare your address on device with address shown bellow.

    +
    +
    +

    { account.address }

    + +
    +
    + ); +} +export default ConfirmAddress; + +export const ConfirmUnverifiedAddress = (props: any): any => { + + const account = props.accounts.find(a => a.checksum === props.receive.checksum && a.index === props.receive.accountIndex && a.coin === props.receive.coin); + + const { + onCancel + } = props.modalActions; + + const { + showUnverifiedAddress, + showAddress + } = props.receiveActions; + + + return ( +
    + +

    Your TREZOR is not connected

    +

    To prevent phishing attacks, you should verify the address on your TREZOR first. Please reconnect your device to continue with the verification process.

    + + +
    + ); +} diff --git a/src/js/components/modal/ConfirmSignTx.js b/src/js/components/modal/ConfirmSignTx.js new file mode 100644 index 00000000..1fa9621a --- /dev/null +++ b/src/js/components/modal/ConfirmSignTx.js @@ -0,0 +1,34 @@ +/* @flow */ +'use strict'; + +import React from 'react'; + +const Confirmation = (props): any => { + const { + amount, + address, + coin, + token, + total, + selectedFeeLevel + } = props.sendForm; + + return ( +
    +
    +

    Confirm transaction on your TREZOR

    +

    Details are shown on device

    +
    +
    + +

    { `${amount} ${token.toUpperCase() }` }

    + +

    { address }

    + +

    { selectedFeeLevel.label }

    +
    +
    + ); +} + +export default Confirmation; \ No newline at end of file diff --git a/src/js/components/modal/Confirmation.js b/src/js/components/modal/Confirmation.js deleted file mode 100644 index dbe7552e..00000000 --- a/src/js/components/modal/Confirmation.js +++ /dev/null @@ -1,17 +0,0 @@ -/* @flow */ -'use strict'; - -import React from 'react'; - -const Confirmation = (props): any => { - const { onConfirmation, onConfirmationCancel } = props.modalActions; - return ( -
    -

    Confirm

    - - -
    - ); -} - -export default Confirmation; \ No newline at end of file diff --git a/src/js/components/modal/DuplicateDevice.js b/src/js/components/modal/DuplicateDevice.js new file mode 100644 index 00000000..a8ad19a6 --- /dev/null +++ b/src/js/components/modal/DuplicateDevice.js @@ -0,0 +1,21 @@ +/* @flow */ +'use strict'; + +import React from 'react'; + +const RememberDevice = (props: any): any => { + const { device } = props.modal; + const { onCancel, onDuplicateDevice } = props.modalActions; + return ( +
    +

    Duplicate { device.label } ?

    + + + + + +
    + ); +} + +export default RememberDevice; \ No newline at end of file diff --git a/src/js/components/modal/FeeSelection.js b/src/js/components/modal/FeeSelection.js deleted file mode 100644 index d913a007..00000000 --- a/src/js/components/modal/FeeSelection.js +++ /dev/null @@ -1,95 +0,0 @@ -/* @flow */ -'use strict'; - -import React from 'react'; -import { formatAmount, formatTime } from '../../utils/formatUtils'; - -const FeeSelection = (props): any => { - - const { - onChangeAccount, - onCustomFeeOpen, - onCustomFeeChange, - onFeeSelect - } = props.modalActions; - - const { - feeList, - coinInfo, - customFeeOpened, - customFee - } = props.modal; - - - const feesCollection = feeList.map((feeItem, index) => { - // skip custom - if (feeItem.name === 'custom') return null; - let feeName; - if (feeItem.name === 'normal' && feeItem.bytes > 0) { - feeName = ( -
    - { feeItem.name } - recommended -
    - ); - } else { - feeName = ({ feeItem.name }); - } - - let feeButton: string; - - if (feeItem.fee > 0) { - return ( -
    - -
    - ); - } else { - return ( -
    - -
    - ); - } - }); - - return ( -
    -

    Select fee:

    -
    - Change account -
    -
    - { feesCollection } -
    - -
    -
    - onCustomFeeChange(event.target.value) } /> -
    sat/B
    - -
    -
    - Setting custom fee is not recommended. - If you set too low fee, it might get stuck forever. -
    -
    -
    -
    -
    - ); -} - -export default FeeSelection; \ No newline at end of file diff --git a/src/js/components/modal/InvalidPin.js b/src/js/components/modal/InvalidPin.js index 4682a26a..63933764 100644 --- a/src/js/components/modal/InvalidPin.js +++ b/src/js/components/modal/InvalidPin.js @@ -4,9 +4,11 @@ import React from 'react'; const InvalidPin = (props): any => { + const { device } = props.modal; return ( -
    -

    Entered PIN is not correct. Retrying...

    +
    +

    Entered PIN for { device.label } is not correct.

    +

    Retrying...

    ); } diff --git a/src/js/components/modal/Modal.js b/src/js/components/modal/Modal.js deleted file mode 100644 index 760adc58..00000000 --- a/src/js/components/modal/Modal.js +++ /dev/null @@ -1,100 +0,0 @@ -/* @flow */ -'use strict'; - -import React, { Component } from 'react'; -import { CSSTransition, Transition } from 'react-transition-group'; - -import { UI } from 'trezor-connect'; - -import Pin from './Pin'; -import InvalidPin from './InvalidPin'; -import Passphrase from './Passphrase'; -import Permission from './Permission'; -import Confirmation from './Confirmation'; - -import AccountSelection from './AccountSelection'; -import FeeSelection from './FeeSelection'; - -const duration = 300; - -const defaultStyle = { - transition: `opacity ${duration}ms ease-in-out`, - opacity: 0, - padding: 20, - display: 'inline-block', - backgroundColor: '#8787d8' -} - -const transitionStyles = { - entering: { opacity: 0 }, - entered: { opacity: 1 }, -}; - -const Fade2 = ({ in: inProp }) => ( - - {(state) => ( -
    - I'm A fade Transition2 -
    - )} -
    -); - -const Fade = ({ children, ...props }) => ( - - { children } - -); - -export default class Modal extends Component { - render() { - const { opened, windowType } = this.props.modal; - - let component = null; - switch(windowType) { - case UI.REQUEST_PIN : - component = (); - break; - case UI.INVALID_PIN : - component = (); - break; - case UI.REQUEST_PASSPHRASE : - component = (); - break; - case UI.REQUEST_PERMISSION : - component = (); - break; - case UI.REQUEST_CONFIRMATION : - component = (); - break; - - case UI.SELECT_ACCOUNT : - component = (); - break; - case UI.SELECT_FEE : - component = (); - break; - } - - let ch = null; - if (opened) { - ch = ( - -
    -
    - { component } -
    -
    -
    - ); - } - - return ch; - } -} diff --git a/src/js/components/modal/ModalContainer.js b/src/js/components/modal/ModalContainer.js new file mode 100644 index 00000000..ce1266cd --- /dev/null +++ b/src/js/components/modal/ModalContainer.js @@ -0,0 +1,119 @@ +/* @flow */ +'use strict'; + +import React, { Component } from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router-dom'; + +import { CSSTransition, Transition } from 'react-transition-group'; + +import { UI } from 'trezor-connect'; + +import * as ModalActions from '../../actions/ModalActions'; +import * as ReceiveActions from '../../actions/ReceiveActions'; + +import Pin from './Pin'; +import InvalidPin from './InvalidPin'; +import Passphrase from './Passphrase'; +import ConfirmSignTx from './ConfirmSignTx'; +import ConfirmAddress, { ConfirmUnverifiedAddress } from './ConfirmAddress'; +import RememberDevice, { ForgetDevice, DisconnectDevice } from './RememberDevice'; +import DuplicateDevice from './DuplicateDevice'; + +import * as RECEIVE from '../../actions/constants/receive'; +import * as MODAL from '../../actions/constants/Modal'; +import * as CONNECT from '../../actions/constants/TrezorConnect'; + +const duration = 300; + + +const Fade = ({ children, ...props }) => ( + + { children } + +); + +class Modal extends Component { + render() { + const { opened, windowType } = this.props.modal; + + let component = null; + switch (windowType) { + case UI.REQUEST_PIN : + component = (); + break; + case UI.INVALID_PIN : + component = (); + break; + case UI.REQUEST_PASSPHRASE : + component = (); + break; + case "ButtonRequest_SignTx" : + component = () + break; + case "ButtonRequest_Address" : + component = () + break; + case RECEIVE.REQUEST_UNVERIFIED : + component = () + break; + + case CONNECT.REMEMBER_REQUEST : + component = () + break; + + case CONNECT.FORGET_REQUEST : + component = () + break; + + case CONNECT.DISCONNECT_REQUEST : + component = () + break; + + case CONNECT.TRY_TO_DUPLICATE : + component = () + break; + } + + let ch = null; + if (opened) { + ch = ( + +
    +
    + { component } +
    +
    +
    + ); + } + + return ch; + } +} + +const mapStateToProps = (state: any, own: any): any => { + return { + modal: state.modal, + accounts: state.accounts, + devices: state.connect.devices, + sendForm: state.sendForm, + receive: state.receive, + }; +} + +const mapDispatchToProps = (dispatch: any): any => { + return { + modalActions: bindActionCreators(ModalActions, dispatch), + receiveActions: bindActionCreators(ReceiveActions, dispatch), + }; +} + +// export default connect(mapStateToProps, mapDispatchToProps)(Modal); +export default withRouter( + connect(mapStateToProps, mapDispatchToProps)(Modal) +); diff --git a/src/js/components/modal/Passphrase.js b/src/js/components/modal/Passphrase.js index 9c0b8041..dde75b65 100644 --- a/src/js/components/modal/Passphrase.js +++ b/src/js/components/modal/Passphrase.js @@ -1,94 +1,308 @@ /* @flow */ 'use strict'; -import React, { Component, KeyboardEvent, FocusEvent } from 'react'; +import React, { Component } from 'react'; +import raf from 'raf'; + +type State = { + singleInput: boolean; + passphrase: string; + passphraseRevision: string; + passphraseFocused: boolean; + passphraseRevisionFocused: boolean; + passphraseRevisionTouched: boolean; + match: boolean; + visible: boolean; +} export default class PinModal extends Component { - input: HTMLInputElement; + state: State; + passphraseInput: HTMLInputElement; + passphraseRevisionInput: HTMLInputElement; + + constructor(props: any) { + super(props); + + console.warn("PROPZ", props) + const isSavedDevice = props.devices.find(d => d.path === props.modal.device.path && d.remember); + + this.state = { + singleInput: isSavedDevice ? true : false, + passphrase: '', + passphraseRevision: '', + passphraseFocused: false, + passphraseRevisionFocused: false, + passphraseRevisionTouched: false, + match: true, + visible: false + } + } + + keyboardHandler(event: KeyboardEvent): void { + + + if (event.keyCode === 13) { + event.preventDefault(); + //this.passphraseInput.blur(); + //this.passphraseRevisionInput.blur(); + + //this.passphraseInput.type = 'text'; + //this.passphraseRevisionInput.type = 'text'; + + this.submit(); + + // TODO: set timeout, or wait for blur event + //onPassphraseSubmit(passphrase, passphraseCached); + //raf(() => onPassphraseSubmit(passphrase)); + } + } componentDidMount(): void { // one time autofocus - this.input.focus(); + this.passphraseInput.focus(); this.keyboardHandler = this.keyboardHandler.bind(this); window.addEventListener('keydown', this.keyboardHandler, false); + + + + // document.oncontextmenu = (event) => { + // const el = window.event.srcElement || event.target; + // const type = el.tagName.toLowerCase() || ''; + // if (type === 'input') { + // return false; + // } + // }; } componentWillUnmount(): void { window.removeEventListener('keydown', this.keyboardHandler, false); + // this.passphraseInput.type = 'text'; + // this.passphraseInput.style.display = 'none'; + // this.passphraseRevisionInput.type = 'text'; + // this.passphraseRevisionInput.style.display = 'none'; } - keyboardHandler(event: KeyboardEvent): void { - const { onPassphraseSubmit } = this.props; - const { passphrase, passphraseCached } = this.props.modal; - - if (event.keyCode === 13) { - event.preventDefault(); - this.input.blur(); - onPassphraseSubmit(passphrase, passphraseCached); - } - } // we don't want to keep password inside "value" attribute, // so we need to replace it thru javascript componentDidUpdate() { - const { passphrase, passphraseFocused, passphraseVisible } = this.props.modal; - let inputValue: string = passphrase; - if (!passphraseVisible && !passphraseFocused) { - inputValue = passphrase.replace(/./g, '•'); + const { + passphrase, + passphraseRevision, + passphraseFocused, + passphraseRevisionFocused, + visible + } = this.state; + // } = this.props.modal; + + let passphraseInputValue: string = passphrase; + let passphraseRevisionInputValue: string = passphraseRevision; + if (!visible && !passphraseFocused) { + passphraseInputValue = passphrase.replace(/./g, '•'); + } + if (!visible && !passphraseRevisionFocused) { + passphraseRevisionInputValue = passphraseRevision.replace(/./g, '•'); + } + + this.passphraseInput.value = passphraseInputValue; + this.passphraseInput.setAttribute("type", visible ? "text" : "password"); + + if (this.passphraseRevisionInput) { + this.passphraseRevisionInput.value = passphraseRevisionInputValue; + this.passphraseRevisionInput.setAttribute("type", visible ? "text" : "password"); + } + + } + + onPassphraseChange = (input: string, value: string): void => { + // https://codepen.io/MiDri/pen/PGqvrO + // or + // https://github.com/zakangelle/react-password-mask/blob/master/src/index.js + if (input === 'passphrase') { + this.setState({ + match: this.state.singleInput || this.state.passphraseRevision === value, + passphrase: value + }); + } else { + this.setState({ + match: this.state.passphrase === value, + passphraseRevision: value, + passphraseRevisionTouched: true + }); + } + } + + onPassphraseFocus = (input: string): void => { + if (input === 'passphrase') { + this.setState({ + passphraseFocused: true + }); + } else { + this.setState({ + passphraseRevisionFocused: true + }); + } + } + + onPassphraseBlur = (input: string): void => { + if (input === 'passphrase') { + this.setState({ + passphraseFocused: false + }); + } else { + this.setState({ + passphraseRevisionFocused: false + }); } - this.input.value = inputValue; } - render(): void { + onPassphraseShow = (): void => { + this.setState({ + visible: true + }); + } + + onPassphraseHide = (): void => { + this.setState({ + visible: false + }); + } + + submit = (empty: boolean = false): void => { + const { onPassphraseSubmit } = this.props.modalActions; + const { passphrase } = this.state; + + //this.passphraseInput.type = 'text'; + // this.passphraseInput.style.display = 'none'; + //this.passphraseInput.setAttribute('readonly', 'readonly'); + // this.passphraseRevisionInput.type = 'text'; + //this.passphraseRevisionInput.style.display = 'none'; + //this.passphraseRevisionInput.setAttribute('readonly', 'readonly'); + + const p = passphrase; + + this.setState({ + passphrase: '', + passphraseRevision: '', + passphraseFocused: false, + passphraseRevisionFocused: false, + visible: false + }) + + raf(() => onPassphraseSubmit(empty ? '' : passphrase)); + } + + render(): any { const { - onPassphraseChange, - onPassphraseSubmit, - onPassphraseForget, - onPassphraseFocus, - onPassphraseBlur, - onPassphraseSave, - onPassphraseShow, - onPassphraseHide + //onPassphraseChange, + //onPassphraseSubmit, + //onPassphraseSubmitEmpty, + //onPassphraseForget, + //onPassphraseFocus, + //onPassphraseBlur, + //onPassphraseSave, + //onPassphraseShow, + //onPassphraseHide } = this.props.modalActions; - const { passphrase, passphraseFocused, passphraseVisible, passphraseCached } = this.props.modal; - let inputType: string = passphraseVisible || (!passphraseVisible && !passphraseFocused) ? "text" : "password"; - const showPassphraseCheckboxFn: Function = passphraseVisible ? onPassphraseHide : onPassphraseShow; - const savePassphraseCheckboxFn: Function = passphraseCached ? onPassphraseForget : onPassphraseSave; + const { + device, + //passphrase, + //passphraseRevision, + //passphraseFocused, + //passphraseRevisionFocused, + //passphraseVisible, + //passphraseMatch, + //passphraseRevisionTouched, + passphraseCached + } = this.props.modal; + + const { + singleInput, + passphrase, + passphraseRevision, + passphraseFocused, + passphraseRevisionFocused, + visible, + match, + passphraseRevisionTouched, + } = this.state; + + let passphraseInputType: string = visible || (!visible && !passphraseFocused) ? "text" : "password"; + let passphraseRevisionInputType: string = visible || (!visible && !passphraseRevisionFocused) ? "text" : "password"; + passphraseInputType = passphraseRevisionInputType = "text"; + //let passphraseInputType: string = visible || passphraseFocused ? "text" : "password"; + //let passphraseRevisionInputType: string = visible || passphraseRevisionFocused ? "text" : "password"; + + + const showPassphraseCheckboxFn: Function = visible ? this.onPassphraseHide : this.onPassphraseShow; return (
    -

    Please enter your passphrase.

    -

    Note that passphrase is case-sensitive.

    -
    + {/* */} +

    Enter { device.label } passphrase

    +

    Note that passphrase is case-sensitive.

    +
    + { this.input = element; } } - onChange={ event => onPassphraseChange(event.currentTarget.value) } - type={ inputType } + ref={ (element) => { this.passphraseInput = element; } } + onChange={ event => this.onPassphraseChange('passphrase', event.currentTarget.value) } + type={ passphraseInputType } autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false" data-lpignore="true" - onFocus={ onPassphraseFocus } - onBlur={ onPassphraseBlur } + onFocus={ event => this.onPassphraseFocus('passphrase') } + onBlur={ event => this.onPassphraseBlur('passphrase') } + tabIndex="1" />
    -
    - -
    ); } diff --git a/src/js/components/modal/Permission.js b/src/js/components/modal/Permission.js deleted file mode 100644 index 1ed8193d..00000000 --- a/src/js/components/modal/Permission.js +++ /dev/null @@ -1,18 +0,0 @@ -/* @flow */ -'use strict'; - -import React from 'react'; - -const Permission = (props): any => { - const { onPermissionGranted, onPermissionRejected } = props.modalActions; - return ( -
    -

    HOST is requesting permissions to:

    -
    - - -
    - ); -} - -export default Permission; \ No newline at end of file diff --git a/src/js/components/modal/Pin.js b/src/js/components/modal/Pin.js index 5b041915..782c988f 100644 --- a/src/js/components/modal/Pin.js +++ b/src/js/components/modal/Pin.js @@ -3,20 +3,41 @@ import React, { Component, KeyboardEvent } from 'react'; +type State = { + pin: string; +} + export default class Pin extends Component { - componentWillMount(): void { - this.keyboardHandler = this.keyboardHandler.bind(this); - window.addEventListener('keydown', this.keyboardHandler, false); + state: State; + + constructor(props: any) { + super(props); + + this.state = { + pin: '', + } } - componentWillUnmount(): void { - window.removeEventListener('keydown', this.keyboardHandler, false); + onPinAdd = (input: number): void => { + let pin: string = this.state.pin; + if (pin.length < 9) { + pin += input; + this.setState({ + pin: pin + }); + } + } + + onPinBackspace = (): void => { + this.setState({ + pin: this.state.pin.substring(0, this.state.pin.length - 1), + }); } keyboardHandler(event: KeyboardEvent): void { const { onPinAdd, onPinBackspace, onPinSubmit } = this.props.modalActions; - const { pin } = this.props.modal; + const { pin } = this.state; event.preventDefault(); switch (event.keyCode) { @@ -26,76 +47,94 @@ export default class Pin extends Component { break; // backspace case 8 : - onPinBackspace(); + this.onPinBackspace(); break; // numeric and numpad case 49 : case 97 : - onPinAdd(1); + this.onPinAdd(1); break; case 50 : case 98 : - onPinAdd(2); + this.onPinAdd(2); break; case 51 : case 99 : - onPinAdd(3); + this.onPinAdd(3); break; case 52 : case 100 : - onPinAdd(4); + this.onPinAdd(4); break; case 53 : case 101 : - onPinAdd(5); + this.onPinAdd(5); break; case 54 : case 102 : - onPinAdd(6); + this.onPinAdd(6); break; case 55 : case 103 : - onPinAdd(7); + this.onPinAdd(7); break; case 56 : case 104 : - onPinAdd(8); + this.onPinAdd(8); break; case 57 : case 105 : - onPinAdd(9); + this.onPinAdd(9); break; } } - render(): void { - const { onPinAdd, onPinBackspace, onPinSubmit } = this.props.modalActions; - const { pin } = this.props.modal; + + + componentWillMount(): void { + this.keyboardHandler = this.keyboardHandler.bind(this); + window.addEventListener('keydown', this.keyboardHandler, false); + } + + componentWillUnmount(): void { + window.removeEventListener('keydown', this.keyboardHandler, false); + } + + render(): any { + const { onPinSubmit } = this.props.modalActions; + const { device } = this.props.modal; + const { pin } = this.state; + return (
    -

    Please enter your PIN.

    -

    Look at the device for number positions.

    -
    - - - + {/* */} +

    Enter { device.label } PIN

    +

    The PIN layout is displayed on your TREZOR.

    + +
    + +
    -
    - - - + +
    + + +
    -
    - - - +
    + + +
    -
    - - +
    + + +
    -
    + +
    +

    Not sure how PIN works? Learn more

    ); } diff --git a/src/js/components/modal/RememberDevice.js b/src/js/components/modal/RememberDevice.js new file mode 100644 index 00000000..c361282f --- /dev/null +++ b/src/js/components/modal/RememberDevice.js @@ -0,0 +1,102 @@ +/* @flow */ +'use strict'; + +import React, { Component } from 'react'; +import Loader from '../common/LoaderCircle'; + +type Props = { + modal: any; +} + +type State = { + +countdown: number; + ticker?: number; +} + +export default class RememberDevice extends Component { + + state: State; + + constructor(props: any) { + super(props); + + this.state = { + countdown: 10, + } + // this.setState({ + // countdown: 10 + // }); + } + + componentDidMount(): void { + + const ticker = () => { + if (this.state.countdown - 1 <= 0) { + // TODO: possible race condition, + // device could be already connected but it didn't emit Device.CONNECT event yet + window.clearInterval(this.state.ticker); + const { device } = this.props.modal; + this.props.modalActions.onForgetDevice(device); + } else { + this.setState({ + countdown: this.state.countdown - 1 + }); + } + } + + this.setState({ + countdown: 10, + ticker: window.setInterval(ticker, 1000) + }); + + + + //this.keyboardHandler = this.keyboardHandler.bind(this); + //window.addEventListener('keydown', this.keyboardHandler, false); + } + + componentWillUnmount(): void { + //window.removeEventListener('keydown', this.keyboardHandler, false); + if (this.state.ticker) { + window.clearInterval(this.state.ticker); + } + } + + render(): any { + const { device } = this.props.modal; + const { onForgetDevice, onRememberDevice } = this.props.modalActions; + return ( +
    +

    Forget { device.label } ?

    +

    Would you like TREZOR Wallet to forget your device or to remember it, so that it is still visible even while disconnected?

    + + +
    + ); + } +} + +export const ForgetDevice = (props: any): any => { + const { device } = props.modal; + const { onForgetSingleDevice, onCancel } = props.modalActions; + return ( +
    +

    Forget { device.label } ?

    +

    Forgetting only removes the device from the list on the left, your bitcoins are still safe and you can access them by reconnecting your TREZOR again.

    + + +
    + ); +} + +export const DisconnectDevice = (props: any): any => { + const { device } = props.modal; + const { onForgetSingleDevice, onCancel } = props.modalActions; + return ( +
    +

    Unplug { device.label }

    +

    TREZOR Wallet will forget your TREZOR right after you disconnect it.

    + TODO: its not true, actually i've already forget those data!!! +
    + ); +} \ No newline at end of file diff --git a/src/js/components/wallet/Acquire.js b/src/js/components/wallet/Acquire.js new file mode 100644 index 00000000..bf8ff741 --- /dev/null +++ b/src/js/components/wallet/Acquire.js @@ -0,0 +1,20 @@ +/* @flow */ +'use strict'; + +import React from 'react'; + +const Acquire = (props: any): any => { + return ( +
    +
    +
    +

    Device is used in other window

    +

    Do you want to use your device in this window?

    +
    + +
    +
    + ); +} + +export default Acquire; diff --git a/src/js/components/wallet/Bootloader.js b/src/js/components/wallet/Bootloader.js new file mode 100644 index 00000000..c30a8fed --- /dev/null +++ b/src/js/components/wallet/Bootloader.js @@ -0,0 +1,14 @@ +/* @flow */ +'use strict'; + +import React from 'react'; + +const Bootloader = (props: any): any => { + return ( +
    +

    Bootloader mode

    +
    + ); +} + +export default Bootloader; diff --git a/src/js/components/wallet/Dashboard.js b/src/js/components/wallet/Dashboard.js new file mode 100644 index 00000000..6f0935f6 --- /dev/null +++ b/src/js/components/wallet/Dashboard.js @@ -0,0 +1,19 @@ +/* @flow */ +'use strict'; + +import React from 'react'; + +const Dashboard = (props: any): any => { + return ( +
    +

    Dashboard

    +
    +

    Please select your coin

    +

    You will gain access to recieving & sending selected coin

    + Dashboard +
    +
    + ); +} + +export default Dashboard; diff --git a/src/js/components/History.js b/src/js/components/wallet/History.js similarity index 70% rename from src/js/components/History.js rename to src/js/components/wallet/History.js index 650b57f0..45f7ebe4 100644 --- a/src/js/components/History.js +++ b/src/js/components/wallet/History.js @@ -2,7 +2,6 @@ 'use strict'; import React, { Component } from 'react'; -import AddressTab from './AddressTab'; const formatTime = (ts) => { var date = new Date(ts * 1000); @@ -35,46 +34,33 @@ const History = (props): any => { if (pending.length > 0) { pendingTransactions = pending.map((tx, i) => { - const etherscanLink = `https://ropsten.etherscan.io/tx/${ tx.txid }`; + const etherscanLink = `https://ropsten.etherscan.io/tx/${ tx.hash }`; return (
    Details - { tx.txid } + { tx.to } Pending...
    ) }); } - const history = JSON.parse(currentAddress.history).result; - txs = history.map((tx, i) => { - + txs = currentAddress.history.map((tx, i) => { const etherscanLink = `https://ropsten.etherscan.io/tx/${ tx.hash }`; - const txType = tx.from === currentAddress.address ? 'out' : 'in'; - const txAddress = txType === 'out' ? tx.to : tx.from; return ( - - -
    - Details +
    + Details { formatTime( parseInt(tx.timeStamp) ) } - { txAddress } + { tx.address } { web3.fromWei(tx.value, 'ether') }
    - // - // { `Account #${(address.index + 1 )}` } - // { address.balance } ETH - // { address.address } - // ) }) } return (
    - - { pendingTransactions ?

    Pending:

    diff --git a/src/js/components/wallet/Receive.js b/src/js/components/wallet/Receive.js new file mode 100644 index 00000000..b39a4a22 --- /dev/null +++ b/src/js/components/wallet/Receive.js @@ -0,0 +1,88 @@ +/* @flow */ +'use strict'; + +import React, { Component } from 'react'; +import { QRCode } from 'react-qr-svg'; +import AbstractAccount from './account/AbstractAccount'; +import { Notification } from '../common/Notification'; +import Tooltip from 'rc-tooltip'; + +export default class Receive extends AbstractAccount { + render() { + return super.render(this.props.receive) || _render(this.props); + } +} + +const _render = (props: any): any => { + + const { + checksum, + accountIndex, + coin, + addressVerified, + addressUnverified, + } = props.receive; + + const device = props.devices.find(d => d.checksum === checksum); + const account = props.accounts.find(a => a.checksum === checksum && a.index === accountIndex && a.coin === coin); + + let qrCode = null; + let address = `${account.address.substring(0, 20)}...`; + let className = 'address hidden'; + let button = ( + + ); + + if (addressVerified || addressUnverified) { + qrCode = ( + + ); + address = account.address; + className = addressUnverified ? 'address unverified' : 'address'; + + const tooltip = addressUnverified ? + (
    Unverified address.
    { device.connected ? 'Show on TREZOR' : 'Connect your TREZOR to verify it.' }
    ) + : + (
    { device.connected ? 'Show on TREZOR' : 'Connect your TREZOR to verify address.' }
    ); + + button = ( +
    } + overlay={ tooltip } + placement="bottomRight"> + + + ); + } + + return ( +
    + { !device.connected ? ( + + ) : null } +

    Receive Ethereum or tokens

    + +
    +
    + { address } +
    + { button } +
    + { qrCode } +
    + ); + + +} + diff --git a/src/js/components/wallet/Settings.js b/src/js/components/wallet/Settings.js new file mode 100644 index 00000000..27d5535e --- /dev/null +++ b/src/js/components/wallet/Settings.js @@ -0,0 +1,12 @@ +/* @flow */ +'use strict'; + +import React from 'react'; + +export default (props: any): any => { + return ( +
    + Settings +
    + ); +} diff --git a/src/js/components/wallet/SignVerify.js b/src/js/components/wallet/SignVerify.js new file mode 100644 index 00000000..d07e41d5 --- /dev/null +++ b/src/js/components/wallet/SignVerify.js @@ -0,0 +1,29 @@ +/* @flow */ +'use strict'; + +import React from 'react'; + +export default (props: any): any => { + return ( +
    +
    +

    Sign message

    + + + + + + +
    +
    +

    Verify message

    + + + + + + +
    +
    + ); +} diff --git a/src/js/components/wallet/account/AbstractAccount.js b/src/js/components/wallet/account/AbstractAccount.js new file mode 100644 index 00000000..9ad898ee --- /dev/null +++ b/src/js/components/wallet/account/AbstractAccount.js @@ -0,0 +1,80 @@ +/* @flow */ +'use strict'; + +import React, { Component } from 'react'; +import { Notification } from '../../common/Notification'; + +export default class AbstractAccount extends Component { + + componentDidMount() { + this.props.initAccount(); + } + + componentWillUpdate(newProps: any) { + this.props.updateAccount(); + } + + // shouldInitAccount(newProps: any): boolean { + // const locationChanged: boolean = newProps.location.pathname !== this.props.location.pathname; + // const accountNotLoaded: boolean = !newProps.detail.loaded && !this.props.detail.loaded; + // return (locationChanged || accountNotLoaded); + // } + + // shouldUpdateAccount(newProps: any): boolean { + // const { detail } = this.props; + // const loaded: boolean = detail.loaded; + + // if (detail.address === '') { + // const currentAccount = this.props.accounts.find(a => a.index === detail.addressIndex && a.coin === detail.coin && a.checksum === detail.checksum); + + // } + + + // // return (loaded && ); + // } + + componentWillUnmount() { + this.props.disposeAccount(); + } + + render(state: any): any { + + const props = this.props; + + if (!state.checksum) { + return (
    ); + } + + const device = this.props.devices.find(d => d.checksum === state.checksum); + const discovery = props.discovery.find(d => d.checksum === device.checksum && d.coin === state.coin); + const account = props.accounts.find(a => a.checksum === state.checksum && a.index === state.accountIndex && a.coin === state.coin); + + if (!account) { + if (!discovery || discovery.waitingForDevice) { + return ( +
    + +
    + ); + } else if (discovery.completed) { + return ( +
    + +
    + ); + } else { + return ( +
    + +
    + ); + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/js/components/wallet/account/AccountTabs.js b/src/js/components/wallet/account/AccountTabs.js new file mode 100644 index 00000000..34f4c086 --- /dev/null +++ b/src/js/components/wallet/account/AccountTabs.js @@ -0,0 +1,34 @@ +/* @flow */ +'use strict'; + +import React from 'react'; +import { NavLink } from 'react-router-dom'; + +const AccountTabs = (props: any): any => { + + const urlParams = props.match.params; + //const urlParams = props.match ? props.match.params : { address: '0' }; + const basePath = `/device/${urlParams.device}/coin/${urlParams.coin}/address/${urlParams.address}`; + + return ( +
    + {/* + History + */} + + Summary + + + Send + + + Receive + + + Sign & Verify + +
    + ); +} + +export default AccountTabs; \ No newline at end of file diff --git a/src/js/components/wallet/aside/AccountSelection.js b/src/js/components/wallet/aside/AccountSelection.js new file mode 100644 index 00000000..8a8bc53e --- /dev/null +++ b/src/js/components/wallet/aside/AccountSelection.js @@ -0,0 +1,98 @@ +/* @flow */ +'use strict'; + +import React, { PureComponent } from 'react'; +import { Link, NavLink } from 'react-router-dom'; +import BigNumber from 'bignumber.js'; + +import { getAccounts } from '../../../utils/reducerUtils'; +import { findSelectedDevice } from '../../../reducers/TrezorConnectReducer'; +import Loader from '../../common/LoaderCircle'; + +const AccountSelection = (props: any): any => { + + const selected = findSelectedDevice(props.connect); + if (!selected) return null; + + const { location } = props.router; + const accounts = props.accounts; + const baseUrl: string = `/device/${location.params.device}`; + const fiatRate = props.fiatRate || '1'; + + // console.warn("AccountSelectionRender", selected, props); + + const deviceAddresses: Array = getAccounts(accounts, selected, location.params.coin); + let selectedAccounts = deviceAddresses.map((address, i) => { + // const url: string = `${baseUrl}/coin/${location.params.coin}/address/${i}`; + const url: string = location.pathname.replace(/address+\/([0-9]*)/, `address/${i}`); + const b = new BigNumber(address.balance); + const fiat = b.times(fiatRate).toFixed(2); + const balance = address.balance !== '' ? `${ address.balance } ${ location.params.coin.toUpperCase() } / $${ fiat }` : 'Loading...'; + return ( + + { `Address #${(address.index + 1 )}` } + { address.loaded ? balance : "Loading..." } + + ) + }); + + if (selectedAccounts.length < 1) { + if (selected.connected) { + const url: string = location.pathname.replace(/address+\/([0-9]*)/, `address/0`); + selectedAccounts = ( + + Address #1 + Loading... + + ) + } + } + + let discoveryStatus = null; + const discovery = props.discovery.find(d => d.checksum === selected.checksum && d.coin === location.params.coin); + + if (discovery) { + if (discovery.completed) { + // TODO: add only if last one is not empty + discoveryStatus = ( +
    + Add address +
    + ) + } else if (!selected.connected) { + discoveryStatus = ( +
    + Addresses could not be loaded + { `Connect ${ selected.instanceLabel } device` } +
    + ) + } else { + discoveryStatus = ( +
    + Loading accounts... +
    + ) + } + } + + const { config } = props.localStorage; + const selectedCoin = config.coins.find(c => c.shortcut === location.params.coin); + let backButton = null; + if (selectedCoin) { + backButton = ( + + { selectedCoin.name } + + ); + } + + return ( +
    + { backButton } + { selectedAccounts } + { discoveryStatus } +
    + ); +} + +export default AccountSelection; \ No newline at end of file diff --git a/src/js/components/wallet/aside/Aside.js b/src/js/components/wallet/aside/Aside.js new file mode 100644 index 00000000..52df1446 --- /dev/null +++ b/src/js/components/wallet/aside/Aside.js @@ -0,0 +1,82 @@ +/* @flow */ +'use strict'; + +import React from 'react'; +import { Link, NavLink } from 'react-router-dom'; +import { TransitionGroup, CSSTransition } from 'react-transition-group'; + +import { DeviceSelect, DeviceDropdown } from './DeviceSelection'; +import AccountSelection from './AccountSelection'; +import CoinSelection from './CoinSelection'; +import StickyContainer from './StickyContainer'; +import { findSelectedDevice } from '../../../reducers/TrezorConnectReducer'; + +const TransitionMenu = (props: any) => { + return ( + + { window.dispatchEvent( new Event('resize') ) } } + onExited= { () => window.dispatchEvent( new Event('resize') ) } + in={ true } + out={ true } + classNames={ props.animationType } + appear={false} + timeout={ 300 }> + { props.children } + + + ) +} + +const Aside = (props: any): any => { + + const selected = findSelectedDevice(props.connect); + const { location } = props.router; + + if (location.pathname === '/' || !selected) return (); + + // TODO + // if (selectedDevice.unacquired) { + // return ( + // + // ); + // } + + let menu = null; + + if (props.deviceDropdownOpened) { + menu = ; + } else if (location.params.coin) { + menu = ( + + + + ); + } else if (!selected.unacquired) { + menu = ( + + + + ); + } + + console.warn("ASIDEE", props) + + return ( + + + { menu } + + + ) +} + +export default Aside; \ No newline at end of file diff --git a/src/js/components/wallet/aside/CoinSelection.js b/src/js/components/wallet/aside/CoinSelection.js new file mode 100644 index 00000000..a28692a4 --- /dev/null +++ b/src/js/components/wallet/aside/CoinSelection.js @@ -0,0 +1,49 @@ +/* @flow */ +'use strict'; + +import React from 'react'; +import { Link, NavLink } from 'react-router-dom'; + +const CoinSelection = (props: any): any => { + const { location } = props.router; + const { config } = props.localStorage; + + const walletCoins = config.coins.map(item => { + const url = `${ location.pathname }/coin/${ item.shortcut }/address/0`; + const className = `coin ${ item.shortcut }` + return ( + + { item.name } + + ) + }) + + return ( +
    + { walletCoins } +
    + Other coins (You will be redirected) +
    + + Bitcoin + + + Litecoin + + + Bitcoin Cash + + + Bitcoin Gold + + + Dash + + + Zcash + +
    + ); +} + +export default CoinSelection; \ No newline at end of file diff --git a/src/js/components/wallet/aside/DeviceSelection.js b/src/js/components/wallet/aside/DeviceSelection.js new file mode 100644 index 00000000..35efd0d9 --- /dev/null +++ b/src/js/components/wallet/aside/DeviceSelection.js @@ -0,0 +1,140 @@ +/* @flow */ +'use strict'; + +import React from 'react'; +import Select from 'react-select'; + +import { findSelectedDevice } from '../../../reducers/TrezorConnectReducer'; + + +const Value = (props: any): any => { + const device = props.value; // device is passed as value of selected item + + // prevent onMouseDown event + const onMouseDown = event => { + if (props.onClick) { + event.preventDefault(); + event.stopPropagation(); + } + } + + const onClick = (item, device) => { + if (props.onClick) + props.onClick(item, device); + } + + let deviceStatus: string = "Connected"; + let css: string = "device"; + const deviceMenuItems: Array = []; + // deviceMenuItems.push("settings"); + + if (device.unacquired) { + css += " unacquired"; + deviceStatus = "Used in other window"; + } + if (device.isUsedElsewhere) { + css += " used-elsewhere"; + deviceStatus = "Used in other window"; + deviceMenuItems.push("acquire"); + } else if (device.featuresNeedsReload) { + css += " reload-features"; + //deviceMenuItems.push("acquire"); + } + if (!device.connected) { + css += " reload-features"; + deviceStatus = "Disconnected"; + } + + if (device.remember) { + deviceMenuItems.push("forget"); + } + + const deviceMenuButtons = deviceMenuItems.map((item, index) => { + return ( +
    onClick(item, device) }>
    + ) + }); + const deviceMenu = deviceMenuButtons.length < 1 ? null : ( +
    + { deviceMenuButtons } +
    + ); + + return ( +
    +
    + { device.instanceLabel } + { deviceStatus } +
    + { deviceMenu } +
    + ); +} + +export const DeviceSelect = (props: any): any => { + + const { devices } = props.connect; + const selected = findSelectedDevice(props.connect); + if (!selected) return null; + + const handleMenuClick = (type, device) => { + console.log("handleMenuClick", type, device) + if (type === 'acquire') { + props.acquireDevice(device); + } else if (type === 'forget') { + props.forgetDevice(device); + }else if (type === 'settings') { + props.duplicateDevice(device); + } + } + + return ( + onGasLimitChange(event.target.value) } /> + { errors.gasLimit ? ({ errors.gasLimit }) : null } + { warnings.gasLimit ? ({ warnings.gasLimit }) : null } +
    +
    +
    } + overlay={ gasPriceTooltip } + placement="top"> + + + + onGasPriceChange(event.target.value) } /> + { errors.gasPrice ? ({ errors.gasPrice }) : null } +
    +
    + +
    +
    } + overlay={ dataTooltip } + placement="top"> + + + + + { errors.data ? ({ errors.data }) : null } +
    + +
    + { props.children } +
    + + + +
    + ) +} + +export default AdvancedForm; \ No newline at end of file diff --git a/src/js/components/wallet/send/CoinSelectOption.js b/src/js/components/wallet/send/CoinSelectOption.js new file mode 100644 index 00000000..d98b0652 --- /dev/null +++ b/src/js/components/wallet/send/CoinSelectOption.js @@ -0,0 +1,60 @@ +/* @flow */ +'use strict'; + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + + +// export default (props: any): any => { +// console.log("RENDER CUSTOM OPTION", props) +// return ( +//
    1
    +// ) +// } + +class FeeSelectOption extends Component { + constructor(props) { + super(props); + } + + handleMouseDown(event) { + event.preventDefault(); + event.stopPropagation(); + this.props.onSelect(this.props.option, event); + } + + handleMouseEnter(event) { + this.props.onFocus(this.props.option, event); + } + + handleMouseMove(event) { + if (this.props.isFocused) return; + this.props.onFocus(this.props.option, event); + } + + render() { + const css = `${this.props.className} ${this.props.option.value}`; + return ( +
    + { this.props.children } +
    + ); + } +} + +FeeSelectOption.propTypes = { + children: PropTypes.node, + className: PropTypes.string, + isDisabled: PropTypes.bool, + isFocused: PropTypes.bool, + isSelected: PropTypes.bool, + onFocus: PropTypes.func, + onSelect: PropTypes.func, + option: PropTypes.object.isRequired, +}; + +export default FeeSelectOption; \ No newline at end of file diff --git a/src/js/components/wallet/send/FeeSelect.js b/src/js/components/wallet/send/FeeSelect.js new file mode 100644 index 00000000..6877996a --- /dev/null +++ b/src/js/components/wallet/send/FeeSelect.js @@ -0,0 +1,63 @@ +/* @flow */ +'use strict'; + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + + +export const FeeSelectValue = (props: any): any => { + return ( +
    +
    + { props.value.value } + { props.value.label } +
    +
    + ); +} + +export class FeeSelectOption extends Component { + constructor(props) { + super(props); + } + + handleMouseDown(event) { + event.preventDefault(); + event.stopPropagation(); + this.props.onSelect(this.props.option, event); + } + + handleMouseEnter(event) { + this.props.onFocus(this.props.option, event); + } + + handleMouseMove(event) { + if (this.props.isFocused) return; + this.props.onFocus(this.props.option, event); + } + + render() { + return ( +
    + { this.props.option.value } + { this.props.option.label } +
    + ); + } +} + +FeeSelectOption.propTypes = { + children: PropTypes.node, + className: PropTypes.string, + isDisabled: PropTypes.bool, + isFocused: PropTypes.bool, + isSelected: PropTypes.bool, + onFocus: PropTypes.func, + onSelect: PropTypes.func, + option: PropTypes.object.isRequired, +}; + + diff --git a/src/js/components/wallet/send/FeeSelectOption.js b/src/js/components/wallet/send/FeeSelectOption.js new file mode 100644 index 00000000..58481ac0 --- /dev/null +++ b/src/js/components/wallet/send/FeeSelectOption.js @@ -0,0 +1,60 @@ +/* @flow */ +'use strict'; + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + + +// export default (props: any): any => { +// console.log("RENDER CUSTOM OPTION", props) +// return ( +//
    1
    +// ) +// } + +class FeeSelectOption extends Component { + constructor(props) { + super(props); + } + + handleMouseDown(event) { + event.preventDefault(); + event.stopPropagation(); + this.props.onSelect(this.props.option, event); + } + + handleMouseEnter(event) { + this.props.onFocus(this.props.option, event); + } + + handleMouseMove(event) { + if (this.props.isFocused) return; + this.props.onFocus(this.props.option, event); + } + + render() { + return ( +
    + { this.props.children } + + $10.20 / 8.828392159996002 ETH +
    + ); + } +} + +FeeSelectOption.propTypes = { + children: PropTypes.node, + className: PropTypes.string, + isDisabled: PropTypes.bool, + isFocused: PropTypes.bool, + isSelected: PropTypes.bool, + onFocus: PropTypes.func, + onSelect: PropTypes.func, + option: PropTypes.object.isRequired, +}; + +export default FeeSelectOption; \ No newline at end of file diff --git a/src/js/components/wallet/send/PendingTransactions.js b/src/js/components/wallet/send/PendingTransactions.js new file mode 100644 index 00000000..e69de29b diff --git a/src/js/components/wallet/send/SendForm.js b/src/js/components/wallet/send/SendForm.js new file mode 100644 index 00000000..7527344f --- /dev/null +++ b/src/js/components/wallet/send/SendForm.js @@ -0,0 +1,177 @@ +/* @flow */ +'use strict'; + +import React, { Component } from 'react'; +import Select from 'react-select'; +import AdvancedForm from './AdvancedForm'; +import { FeeSelectValue, FeeSelectOption } from './FeeSelect'; +import { Notification } from '../../common/Notification'; +import AbstractAccount from '../account/AbstractAccount'; + +export default class Send extends AbstractAccount { + render() { + return super.render(this.props.sendForm) || _render(this.props); + } +} + + +const _render = (props: any): any => { + + const device = props.devices.find(d => d.checksum === props.sendForm.checksum); + const discovery = props.discovery.find(d => d.checksum === device.checksum && d.coin === props.sendForm.coin); + const account = props.accounts.find(a => a.checksum === props.sendForm.checksum && a.index === props.sendForm.accountIndex && a.coin === props.sendForm.coin); + const addressTokens = props.tokens.filter(t => t.ethAddress === account.address); + + const { + address, + amount, + setMax, + coin, + token, + feeLevels, + fee, + selectedFeeLevel, + gasPriceNeedsUpdate, + total, + errors, + warnings, + infos, + advanced, + sending, + sendingStatus + } = props.sendForm; + + const { + onAddressChange, + onAmountChange, + onSetMax, + onCurrencyChange, + onFeeLevelChange, + updateFeeLevels, + onSend, + } = props.sendFormActions; + + //const addressTokens = props.tokens.filter(t => t.ethAddress === currentAccount.address); + const tokens = addressTokens.map(t => { + return { value: t.symbol, label: t.symbol }; + }); + tokens.unshift({ value: coin, label: coin.toUpperCase() }); + + const setMaxClassName: string = setMax ? 'set-max enabled' : 'set-max'; + + let updateFeeLevelsButton = null; + if (gasPriceNeedsUpdate) { + updateFeeLevelsButton = ( + Recommended fees updated. Click here to use them + ) + } + + let addressClassName: ?string; + if (errors.address) { + addressClassName = 'not-valid'; + } else if (warnings.address) { + addressClassName = 'warning'; + } else if (address.length > 0) { + addressClassName = 'valid'; + } + + let buttonDisabled: boolean = Object.keys(errors).length > 0 || total === '0' || address.length === 0 || sending; + let buttonLabel: string = 'Send'; + if (coin !== token && amount.length > 0 && !errors.amount) { + buttonLabel += ` ${amount} ${ token.toUpperCase() }` + } else if (coin === token && total !== '0') { + buttonLabel += ` ${total} ${ token.toUpperCase() }`; + } + + //const device = props.devices.find(d => d.checksum === currentAccount.checksum); + if (device && !device.connected) { + buttonLabel = 'Device is not connected'; + buttonDisabled = true; + } + + let notification = null; + // if (sendingStatus) { + // if (sendingStatus.success) { + // notification = (); + // } else { + // notification = (); + // } + // } + + return ( +
    + + { !device.connected ? ( + + ) : null } + +

    Send Ethereum or tokens

    +
    + + onAddressChange(event.target.value) } /> + + { errors.address ? ({ errors.address }) : null } + { warnings.address ? ({ warnings.address }) : null } + { infos.address ? ({ infos.address }) : null } +
    + +
    + +
    + onAmountChange(event.target.value) } /> + + Set max + + +
    + + + + + +
    + ); +} diff --git a/src/js/components/wallet/summary/Summary.1.js b/src/js/components/wallet/summary/Summary.1.js new file mode 100644 index 00000000..463c41c1 --- /dev/null +++ b/src/js/components/wallet/summary/Summary.1.js @@ -0,0 +1,248 @@ +/* @flow */ +'use strict'; + +import React, { Component } from 'react'; +import BigNumber from 'bignumber.js'; +import ColorHash from 'color-hash'; +import ScaleText from 'react-scale-text'; +import Blockies from 'react-blockies'; +import { Async } from 'react-select'; +import { resolveAfter } from '../../../utils/promiseUtils'; +import AbstractAccount from '../account/AbstractAccount'; +import { Notification } from '../Notification'; + + +export default class Summary extends AbstractAccount { + + componentDidMount() { + super.componentDidMount(); + //this.props.summaryActions.init(); + } + + componentWillUpdate(newProps: any) { + super.componentWillUpdate(newProps); + //if (newProps.location.pathname !== this.props.location.pathname || (!newProps.summary.loaded && !this.props.summary.loaded)) { + //if (newProps.router.pathname !== this.props.router.pathname || (!newProps.summary.loaded && !this.props.summary.loaded)) { + // this.props.summaryActions.init(); + //} + } + + componentWillUnmount() { + super.componentWillUnmount(); + //this.props.summaryActions.dispose(); + } + + render() { + return _render(this.props); + } +} + +const _render = (props: any): any => { + + const currentAccount = props.account; + const fiatRate = props.fiatRate || '1030'; + + const { + loaded, + address, + summary, + addForm, + search, + customAddress, + customName, + customShortcut, + customDecimal, + + selectedToken + } = props.summary; + + if (currentAccount.deviceStateError) { + return ( +
    + +
    + ); + } + + + // if (!loaded) return null; + + const { + onSummaryToggle, + onTokenSearch, + onCustomTokenToggle, + onCustomTokenAddressChange, + onCustomTokenNameChange, + onCustomTokenShortcutChange, + onCustomTokenDecimalChange, + onCustomTokenAdd + } = props.summaryActions; + + const tokens = props.tokens.filter(t => t.ethAddress === address); + + + let summaryClassName: string = "summary closed"; + let summaryContent = null; + if (summary) { + summaryClassName = "summary"; + if (currentAccount && currentAccount.balance) { + + const balance = new BigNumber(currentAccount.balance); + const fiat = balance.times(fiatRate).toFixed(2); + + summaryContent = ( +
    +
    +
    Balance
    +
    ${ fiat }
    +
    { currentAccount.balance } ETH
    +
    +
    +
    Rate
    +
    ${ fiatRate }
    +
    1.00 ETH
    +
    +
    + ) + } else { + summaryContent = ( +
    +
    +
    Balance
    +
    Loading...
    +
    Loading...
    +
    +
    +
    Rate
    +
    ${ fiatRate }
    +
    1.00 ETH
    +
    +
    + ) + } + + } + + let addFormClassName = "add-token-form closed"; + let addFormContent = null; + if (addForm) { + addFormClassName = "add-token-form"; + addFormContent = ( +
    +
    + + onCustomTokenAddressChange(event.target.value) } /> +
    +
    + + onCustomTokenNameChange(event.target.value) } /> +
    +
    + + onCustomTokenShortcutChange(event.target.value) } /> +
    +
    + + onCustomTokenDecimalChange(event.target.value) } /> +
    +
    + +
    +
    + ) + } + + const bg = new ColorHash({lightness: 0.7}); + //const colorHash2 = new ColorHash({lightness: 0.5}); + const colorHash2 = new ColorHash(); + + console.log("SUM", tokens, address, props.tokens) + //let tokensContent = null; + let tokensContent = tokens.map((t, i) => { + + // if (search.length > 0) { + // if (t.name.toLowerCase().indexOf(search) < 0 && t.shortcut.toLowerCase().indexOf(search) < 0) return null; + // } + let iconColor = { + color: colorHash2.hex(t.name), + background: bg.hex(t.name), + borderColor: bg.hex(t.name) + } + return ( +
    +
    +
    +

    { t.symbol }

    +
    +
    +
    { t.name }
    +
    { t.balance }
    +
    + ) + }); + + let ethIcon = null; + if (currentAccount) { + ethIcon = ( + + ); + } + + return ( + +
    +

    { ethIcon } Address #{ parseInt(props.match.params.address) + 1 }

    + +
    + { summaryContent } +
    +
    + +
    + {/* onTokenSearch(event.target.value) } /> */} + 0x58cda554935e4a1f2acbe15f8757400af275e084 + { + console.log("FILTERRR", opt, str, values); + return opt; + } + } + + + value={ selectedToken } + onChange={ props.summaryActions.selectToken } + valueKey="symbol" + labelKey="symbol" + placeholder="Search for token" + loadOptions={ props.summaryActions.loadTokens } + backspaceRemoves={true} /> + +
    + +
    +
    + Add token +
    + { addFormContent } +
    + +
    + { tokensContent } +
    +
    + + ); +} + +const onChange = () => { +} + +const gotoUser = () => { +} \ No newline at end of file diff --git a/src/js/components/wallet/summary/Summary.js b/src/js/components/wallet/summary/Summary.js new file mode 100644 index 00000000..5c583fc7 --- /dev/null +++ b/src/js/components/wallet/summary/Summary.js @@ -0,0 +1,74 @@ +/* @flow */ +'use strict'; + +import React, { Component } from 'react'; +import BigNumber from 'bignumber.js'; +import { Async } from 'react-select'; +import { resolveAfter } from '../../../utils/promiseUtils'; +import AbstractAccount from '../account/AbstractAccount'; +import { Notification } from '../../common/Notification'; +import SummaryDetails from './SummaryDetails.js'; +import SummaryTokens from './SummaryTokens.js'; + + +export default class Summary extends AbstractAccount { + render() { + return super.render(this.props.summary) || _render(this.props); + } +} + +const _render = (props: any): any => { + + const device = props.devices.find(d => d.checksum === props.summary.checksum); + const discovery = props.discovery.find(d => d.checksum === device.checksum && d.coin === props.summary.coin); + const account = props.accounts.find(a => a.checksum === props.summary.checksum && a.index === props.summary.accountIndex && a.coin === props.summary.coin); + const tokens = props.tokens.filter(t => t.ethAddress === account.address); + + return ( + +
    + { !device.connected ? ( + + ) : null } + +

    Address #{ parseInt(props.match.params.address) + 1 }

    + + + +
    + 0x58cda554935e4a1f2acbe15f8757400af275e084 + { + console.log("TODO: filter already added", opt, str, values); + return opt; + } + } + + + value={ props.summary.selectedToken } + onChange={ token => props.summaryActions.selectToken(token, account) } + valueKey="symbol" + labelKey="symbol" + placeholder="Search for token" + loadOptions={ props.summaryActions.loadTokens } + backspaceRemoves={true} /> + +
    + + + +
    + + ); +} \ No newline at end of file diff --git a/src/js/components/wallet/summary/SummaryContainer.js b/src/js/components/wallet/summary/SummaryContainer.js new file mode 100644 index 00000000..18d6717f --- /dev/null +++ b/src/js/components/wallet/summary/SummaryContainer.js @@ -0,0 +1,32 @@ +/* @flow */ +'use strict'; + +import React, { Component, PropTypes } from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; + +import Summary from './Summary'; +import * as SummaryActions from '../../../actions/SummaryActions'; + +function mapStateToProps(state, own) { + return { + location: state.router.location, + devices: state.connect.devices, + accounts: state.accounts, + discovery: state.discovery, + tokens: state.tokens, + summary: state.summary, + fiatRate: state.web3.fiatRate + }; +} + +function mapDispatchToProps(dispatch) { + return { + summaryActions: bindActionCreators(SummaryActions, dispatch), + initAccount: bindActionCreators(SummaryActions.init, dispatch), + updateAccount: bindActionCreators(SummaryActions.update, dispatch), + disposeAccount: bindActionCreators(SummaryActions.dispose, dispatch), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(Summary) \ No newline at end of file diff --git a/src/js/components/wallet/summary/SummaryDetails.js b/src/js/components/wallet/summary/SummaryDetails.js new file mode 100644 index 00000000..f685f838 --- /dev/null +++ b/src/js/components/wallet/summary/SummaryDetails.js @@ -0,0 +1,37 @@ +/* @flow */ +'use strict'; + +import React from 'react'; + +const SummaryDetails = (props: any): any => { + + if (!props.summary.details) { + return ( +
    +
    +
    + ) + } + + const fiatValue = "0"; + + return ( +
    +
    +
    +
    Balance
    +
    ${ fiatValue }
    +
    { props.balance } ETH
    +
    +
    +
    Rate
    +
    ${ props.fiatRate }
    +
    1.00 ETH
    +
    +
    +
    +
    + ); +} + +export default SummaryDetails; \ No newline at end of file diff --git a/src/js/components/wallet/summary/SummaryTokens.js b/src/js/components/wallet/summary/SummaryTokens.js new file mode 100644 index 00000000..22c261e3 --- /dev/null +++ b/src/js/components/wallet/summary/SummaryTokens.js @@ -0,0 +1,46 @@ +/* @flow */ +'use strict'; + +import React from 'react'; +import ColorHash from 'color-hash'; +import ScaleText from 'react-scale-text'; + +const SummaryTokens = (props: any): any => { + + if (!props.tokens || props.tokens.length < 1) return null; + + const bgColor = new ColorHash({lightness: 0.7}); + const textColor = new ColorHash(); + + const tokens = props.tokens.map((t, i) => { + + // if (search.length > 0) { + // if (t.name.toLowerCase().indexOf(search) < 0 && t.shortcut.toLowerCase().indexOf(search) < 0) return null; + // } + let iconColor = { + color: textColor.hex(t.name), + background: bgColor.hex(t.name), + borderColor: bgColor.hex(t.name) + } + return ( +
    +
    +
    +

    { t.symbol }

    +
    +
    +
    { t.name }
    +
    { t.balance }
    +
    + ) + }); + + return ( +
    + { tokens } +
    + ) + +} + +export default SummaryTokens; \ No newline at end of file diff --git a/src/js/containers/DevicesContainer.js b/src/js/containers/AcquireContainer.js similarity index 63% rename from src/js/containers/DevicesContainer.js rename to src/js/containers/AcquireContainer.js index 2aa120cb..31f2f6c7 100644 --- a/src/js/containers/DevicesContainer.js +++ b/src/js/containers/AcquireContainer.js @@ -5,7 +5,7 @@ import React, { Component, PropTypes } from 'react'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; -import Devices from '../components/Devices'; +import Acquire from '../components/wallet/Acquire'; import * as TrezorConnectActions from '../actions/TrezorConnectActions'; function mapStateToProps(state, own) { @@ -15,9 +15,9 @@ function mapStateToProps(state, own) { } function mapDispatchToProps(dispatch) { - return { - onSelectDevice: bindActionCreators(TrezorConnectActions.onSelectDevice, dispatch) + return { + acquireDevice: bindActionCreators(TrezorConnectActions.acquire, dispatch), }; } -export default connect(mapStateToProps, mapDispatchToProps)(Devices); \ No newline at end of file +export default connect(mapStateToProps, mapDispatchToProps)(Acquire); \ No newline at end of file diff --git a/src/js/containers/AddressMenuContainer.js b/src/js/containers/AddressMenuContainer.js deleted file mode 100644 index 86b58c3e..00000000 --- a/src/js/containers/AddressMenuContainer.js +++ /dev/null @@ -1,23 +0,0 @@ -/* @flow */ -'use strict'; - -import React, { Component, PropTypes } from 'react'; -import { bindActionCreators } from 'redux'; -import { connect } from 'react-redux'; -import AddressMenu from '../components/AddressMenu'; -//import * as AccountActions from '../actions/AccountActions'; - -function mapStateToProps(state, own) { - return { - web3: state.web3.web3, - addresses: state.addresses - }; -} - -function mapDispatchToProps(dispatch) { - return { - //onAccountSelect: bindActionCreators(AccountActions.onAccountSelect, dispatch) - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(AddressMenu); \ No newline at end of file diff --git a/src/js/containers/AppContainer.js b/src/js/containers/AppContainer.js deleted file mode 100644 index 686f97a5..00000000 --- a/src/js/containers/AppContainer.js +++ /dev/null @@ -1,30 +0,0 @@ -/* @flow */ -'use strict'; - -import React, { Component } from 'react'; -import { Link } from 'react-router-dom'; -import TrezorConnect from 'trezor-connect'; - -import Devices from './DevicesContainer'; -import Modal from './ModalContainer'; - -import Header from '../components/Header'; -import AddressMenuContainer from './AddressMenuContainer'; -import Footer from '../components/Footer'; - -export default class AppContainer extends Component { - render() { - return ( -
    -
    - -
    - - { this.props.children } -
    -
    - -
    - ); - } -} \ No newline at end of file diff --git a/src/js/containers/AsideContainer.js b/src/js/containers/AsideContainer.js new file mode 100644 index 00000000..d1a66594 --- /dev/null +++ b/src/js/containers/AsideContainer.js @@ -0,0 +1,39 @@ +/* @flow */ +'use strict'; + +import React, { Component, PropTypes } from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router-dom'; +import Aside from '../components/wallet/aside/Aside'; +import * as TrezorConnectActions from '../actions/TrezorConnectActions'; +import { toggleDeviceDropdown } from '../actions/AppActions'; + +function mapStateToProps(state, own) { + return { + connect: state.connect, + accounts: state.accounts, + router: state.router, + deviceDropdownOpened: state.DOM.deviceDropdownOpened, + fiatRate: state.web3.fiatRate, + localStorage: state.localStorage, + discovery: state.discovery + }; +} + +function mapDispatchToProps(dispatch) { + return { + //onAccountSelect: bindActionCreators(AccountActions.onAccountSelect, dispatch), + toggleDeviceDropdown: bindActionCreators(toggleDeviceDropdown, dispatch), + addAddress: bindActionCreators(TrezorConnectActions.addAddress, dispatch), + acquireDevice: bindActionCreators(TrezorConnectActions.acquire, dispatch), + forgetDevice: bindActionCreators(TrezorConnectActions.forget, dispatch), + duplicateDevice: bindActionCreators(TrezorConnectActions.duplicateDevice, dispatch), + onSelectDevice: bindActionCreators(TrezorConnectActions.onSelectDevice, dispatch), + }; +} + +//export default connect(mapStateToProps, mapDispatchToProps)(AddressMenu); +export default withRouter( + connect(mapStateToProps, mapDispatchToProps)(Aside) +); \ No newline at end of file diff --git a/src/js/containers/ModalContainer.js b/src/js/containers/BootloaderContainer.js similarity index 50% rename from src/js/containers/ModalContainer.js rename to src/js/containers/BootloaderContainer.js index 67033faa..6db873cf 100644 --- a/src/js/containers/ModalContainer.js +++ b/src/js/containers/BootloaderContainer.js @@ -5,19 +5,17 @@ import React, { Component, PropTypes } from 'react'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; -import Modal from '../components/modal/Modal'; -import * as ModalActions from '../actions/ModalActions'; +import Bootloader from '../components/wallet/Bootloader'; function mapStateToProps(state, own) { return { - modal: state.modal + }; } function mapDispatchToProps(dispatch) { - return { - modalActions: bindActionCreators(ModalActions, dispatch), + return { }; } -export default connect(mapStateToProps, mapDispatchToProps)(Modal); \ No newline at end of file +export default connect(mapStateToProps, mapDispatchToProps)(Bootloader); \ No newline at end of file diff --git a/src/js/containers/ContentContainer.js b/src/js/containers/ContentContainer.js new file mode 100644 index 00000000..620ebbc6 --- /dev/null +++ b/src/js/containers/ContentContainer.js @@ -0,0 +1,45 @@ +/* @flow */ +'use strict'; + +import React from 'react'; +import { Route } from 'react-router-dom'; + +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router-dom'; +import Log from '../components/common/Log'; +import Notifications from '../components/common/Notification'; +import Footer from '../components/common/Footer'; +import AccountTabs from '../components/wallet/account/AccountTabs'; + +import * as TrezorConnectActions from '../actions/TrezorConnectActions'; + +const Article = (props) => { + return ( +
    + + {/* */} + + { props.children } +
    +
    + ); +} + +function mapStateToProps(state, own) { + return { + + }; +} + +function mapDispatchToProps(dispatch) { + return { + + }; +} + +export default withRouter( + connect(mapStateToProps, mapDispatchToProps)(Article) +); \ No newline at end of file diff --git a/src/js/containers/DashboardContainer.js b/src/js/containers/DashboardContainer.js new file mode 100644 index 00000000..b3cd6406 --- /dev/null +++ b/src/js/containers/DashboardContainer.js @@ -0,0 +1,21 @@ +/* @flow */ +'use strict'; + +import React, { Component, PropTypes } from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; + +import Dashboard from '../components/wallet/Dashboard'; + +function mapStateToProps(state, own) { + return { + + }; +} + +function mapDispatchToProps(dispatch) { + return { + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(Dashboard); \ No newline at end of file diff --git a/src/js/containers/HistoryContainer.js b/src/js/containers/HistoryContainer.js index a597b0fa..7723fea9 100644 --- a/src/js/containers/HistoryContainer.js +++ b/src/js/containers/HistoryContainer.js @@ -5,13 +5,12 @@ import React, { Component, PropTypes } from 'react'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; -import History from '../components/History'; +import History from '../components/wallet/History'; import * as SendFormActions from '../actions/SendFormActions'; function mapStateToProps(state, own) { return { web3: state.web3.web3, - addresses: state.addresses, }; } diff --git a/src/js/containers/LandingPageContainer.js b/src/js/containers/LandingPageContainer.js new file mode 100644 index 00000000..e48f66de --- /dev/null +++ b/src/js/containers/LandingPageContainer.js @@ -0,0 +1,25 @@ +/* @flow */ +'use strict'; + +import React, { Component, PropTypes } from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; + +import LandingPage from '../components/landing/LandingPage'; + +function mapStateToProps(state, own) { + return { + localStorage: state.localStorage, + modal: state.modal, + web3: state.web3, + connect: state.connect, + router: state.router + }; +} + +function mapDispatchToProps(dispatch) { + return { + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(LandingPage); \ No newline at end of file diff --git a/src/js/containers/ReceiveContainer.js b/src/js/containers/ReceiveContainer.js index 1f22d51d..ec729912 100644 --- a/src/js/containers/ReceiveContainer.js +++ b/src/js/containers/ReceiveContainer.js @@ -5,18 +5,26 @@ import React, { Component, PropTypes } from 'react'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; -import Receive from '../components/Receive'; -import * as SendFormActions from '../actions/SendFormActions'; +import Receive from '../components/wallet/Receive'; +import * as ReceiveActions from '../actions/ReceiveActions'; +import { getAddress } from '../actions/TrezorConnectActions'; function mapStateToProps(state, own) { return { - addresses: state.addresses + location: state.router.location, + devices: state.connect.devices, + accounts: state.accounts, + discovery: state.discovery, + receive: state.receive }; } function mapDispatchToProps(dispatch) { return { - sendFormActions: bindActionCreators(SendFormActions, dispatch), + initAccount: bindActionCreators(ReceiveActions.init, dispatch), + updateAccount: bindActionCreators(ReceiveActions.update, dispatch), + disposeAccount: bindActionCreators(ReceiveActions.dispose, dispatch), + showAddress: bindActionCreators(ReceiveActions.showAddress, dispatch), }; } diff --git a/src/js/containers/SendFormContainer.js b/src/js/containers/SendFormContainer.js index 5708e9db..6c63c3e3 100644 --- a/src/js/containers/SendFormContainer.js +++ b/src/js/containers/SendFormContainer.js @@ -5,19 +5,27 @@ import React, { Component, PropTypes } from 'react'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; -import SendForm from '../components/SendForm'; +import SendForm from '../components/wallet/send/SendForm'; import * as SendFormActions from '../actions/SendFormActions'; function mapStateToProps(state, own) { return { - addresses: state.addresses, - sendForm: state.sendForm + location: state.router.location, + devices: state.connect.devices, + accounts: state.accounts, + discovery: state.discovery, + tokens: state.tokens, + sendForm: state.sendForm, + fiatRate: state.web3.fiatRate }; } function mapDispatchToProps(dispatch) { return { sendFormActions: bindActionCreators(SendFormActions, dispatch), + initAccount: bindActionCreators(SendFormActions.init, dispatch), + updateAccount: bindActionCreators(SendFormActions.update, dispatch), + disposeAccount: bindActionCreators(SendFormActions.dispose, dispatch), }; } diff --git a/src/js/containers/SettingsContainer.js b/src/js/containers/SettingsContainer.js new file mode 100644 index 00000000..c63cc894 --- /dev/null +++ b/src/js/containers/SettingsContainer.js @@ -0,0 +1,20 @@ +/* @flow */ +'use strict'; + +import React, { Component, PropTypes } from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; + +import Settings from '../components/wallet/Settings'; + +function mapStateToProps(state, own) { + return { + }; +} + +function mapDispatchToProps(dispatch) { + return { + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(Settings); \ No newline at end of file diff --git a/src/js/containers/SignVerifyContainer.js b/src/js/containers/SignVerifyContainer.js new file mode 100644 index 00000000..8a811e7e --- /dev/null +++ b/src/js/containers/SignVerifyContainer.js @@ -0,0 +1,20 @@ +/* @flow */ +'use strict'; + +import React, { Component, PropTypes } from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; + +import SignVerify from '../components/wallet/SignVerify'; + +function mapStateToProps(state, own) { + return { + }; +} + +function mapDispatchToProps(dispatch) { + return { + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(SignVerify); \ No newline at end of file diff --git a/src/js/containers/TopNavigationContainer.js b/src/js/containers/TopNavigationContainer.js new file mode 100644 index 00000000..95dd0eb4 --- /dev/null +++ b/src/js/containers/TopNavigationContainer.js @@ -0,0 +1,31 @@ +/* @flow */ +'use strict'; + +import React, { Component, PropTypes } from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router-dom'; + +import TopNavigation from '../components/TopNavigation'; +import * as TrezorConnectActions from '../actions/TrezorConnectActions'; +import { resizeAppContainer, toggleDeviceDropdown } from '../actions/AppActions'; + +function mapStateToProps(state, own) { + return { + connect: state.connect + }; +} + +function mapDispatchToProps(dispatch) { + return { + toggleDeviceDropdown: bindActionCreators(toggleDeviceDropdown, dispatch), + resizeAppContainer: bindActionCreators(resizeAppContainer, dispatch), + acquireDevice: bindActionCreators(TrezorConnectActions.acquire, dispatch), + onSelectDevice: bindActionCreators(TrezorConnectActions.onSelectDevice, dispatch), + }; +} + +// export default connect(mapStateToProps, mapDispatchToProps)(TopNavigation); +export default withRouter( + connect(mapStateToProps, mapDispatchToProps)(TopNavigation) +); \ No newline at end of file diff --git a/src/js/containers/WalletContainer.js b/src/js/containers/WalletContainer.js new file mode 100644 index 00000000..d05ef8fe --- /dev/null +++ b/src/js/containers/WalletContainer.js @@ -0,0 +1,40 @@ +/* @flow */ +'use strict'; + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router-dom'; + +import Header from '../components/common/Header'; +import AsideContainer from './AsideContainer'; +import ContentContainer from './ContentContainer'; +import ModalContainer from '../components/modal/ModalContainer'; + +const Wallet = (props: any): any => { + return ( +
    +
    +
    + + + { props.children } + +
    + +
    + ); +} + +function mapStateToProps(state, own) { + return { + + }; +} + +function mapDispatchToProps(dispatch) { + return { }; +} + +export default withRouter( + connect(mapStateToProps, mapDispatchToProps)(Wallet) +); diff --git a/src/js/containers/index.js b/src/js/containers/index.js index 64fe942f..0adc537c 100644 --- a/src/js/containers/index.js +++ b/src/js/containers/index.js @@ -1,7 +1,19 @@ /* @flow */ 'use strict'; -export { default as AppContainer } from './AppContainer'; +// wrapper layouts +export { default as LandingPageContainer } from './LandingPageContainer'; +export { default as WalletContainer } from './WalletContainer'; + +// wallet sections +export { default as AcquireContainer } from './AcquireContainer'; +export { default as BootloaderContainer } from './BootloaderContainer'; + +export { default as DashboardContainer } from './DashboardContainer'; export { default as HistoryContainer } from './HistoryContainer'; export { default as SendFormContainer } from './SendFormContainer'; export { default as ReceiveContainer } from './ReceiveContainer'; +export { default as SignVerifyContainer } from './SignVerifyContainer'; + + +export { default as SettingsContainer } from './SettingsContainer'; diff --git a/src/js/index.js b/src/js/index.js index 66c69b6b..6b77704f 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -3,26 +3,25 @@ import React from 'react'; import { render } from 'react-dom'; -import { Provider } from 'react-redux'; -import { ConnectedRouter } from 'react-router-redux'; -import store, { history } from './store'; +import store from './store'; import router from './router'; -import { onResize } from './actions/DOMActions'; +import { onResize, onBeforeUnload } from './actions/AppActions'; import styles from '../styles/index.less'; render( - - - { router } - - , + router, document.getElementById('root') ); +// handle resize event and pass it to DOM reducer window.addEventListener('resize', () => { - store.dispatch(onResize()); + store.dispatch( onResize() ); }); +window.onbeforeunload = () => { + store.dispatch( onBeforeUnload() ); +} + // workaround // yarn add web3@^0.19.0 \ No newline at end of file diff --git a/src/js/reducers/AccountDetailReducer.js b/src/js/reducers/AccountDetailReducer.js new file mode 100644 index 00000000..5996c4f3 --- /dev/null +++ b/src/js/reducers/AccountDetailReducer.js @@ -0,0 +1,42 @@ +/* @flow */ +'use strict'; + +import * as ACCOUNT from '../actions/constants/account'; +import * as CONNECT from '../actions/constants/TrezorConnect'; + +export type State = { + +index: number; + +checksum: ?string; + +coin: string; + location: string; +} + +export const initialState: State = { + index: 0, + checksum: null, + coin: '', +}; + + +export default (state: State = initialState, action: any): State => { + + switch (action.type) { + + case ACCOUNT.INIT : + return action.state; + + case CONNECT.DEVICE_STATE_EXCEPTION : + return { + ...state, + deviceStateError: true + } + + case ACCOUNT.DISPOSE : + return initialState; + + + default: + return state; + } + +} \ No newline at end of file diff --git a/src/js/reducers/AccountsReducer.js b/src/js/reducers/AccountsReducer.js new file mode 100644 index 00000000..f355e482 --- /dev/null +++ b/src/js/reducers/AccountsReducer.js @@ -0,0 +1,96 @@ +/* @flow */ +'use strict'; + +import * as CONNECT from '../actions/constants/TrezorConnect'; +import * as ADDRESS from '../actions/constants/Address'; + +export type Account = { + loaded: boolean; + +checksum: string; + +coin: string; + +index: number; + +addressPath: Array; + +address: string; + balance: string; + nonce: number; +} + +const initialState: Array = []; + +const createAccount = (state: Array, action: any): Array => { + + // TODO check witch checksum + // check if account was created before + const exist: ?Account = state.find(addr => addr.address === action.address); + if (exist) { + return state; + } + + const address: Account = { + loaded: false, + checksum: action.device.checksum, + coin: action.coin, + index: action.index, + addressPath: action.path, + address: action.address, + balance: '0', + nonce: 0, + } + + const newState: Array = [ ...state ]; + newState.push(address); + return newState; +} + +const removeAccounts = (state: Array, action: any): Array => { + // TODO: all instances od device (multiple checksums) + return state.filter(addr => addr.checksum !== action.device.checksum); +} + +const forgetAccounts = (state: Array, action: any): Array => { + return state.filter(addr => action.accounts.indexOf(addr) === -1); +} + +const setBalance = (state: Array, action: any): Array => { + const index: number = state.findIndex(addr => addr.address === action.address); + const newState: Array = [ ...state ]; + newState[index].loaded = true; + newState[index].balance = action.balance; + return newState; +} + +const setNonce = (state: Array, action: any): Array => { + const index: number = state.findIndex(addr => addr.address === action.address); + const newState: Array = [ ...state ]; + newState[index].loaded = true; + newState[index].nonce = action.nonce; + return newState; +} + +export default (state: Array = initialState, action: any): Array => { + + switch (action.type) { + + case ADDRESS.CREATE : + return createAccount(state, action); + + case CONNECT.FORGET : + case CONNECT.FORGET_SINGLE : + return removeAccounts(state, action); + + //case CONNECT.FORGET_SINGLE : + // return forgetAccounts(state, action); + + case ADDRESS.SET_BALANCE : + return setBalance(state, action); + case ADDRESS.SET_NONCE : + return setNonce(state, action); + + case ADDRESS.FROM_STORAGE : + return action.payload; + + default: + return state; + } + +} \ No newline at end of file diff --git a/src/js/reducers/AddressesReducer.js b/src/js/reducers/AddressesReducer.js deleted file mode 100644 index ff6dfea1..00000000 --- a/src/js/reducers/AddressesReducer.js +++ /dev/null @@ -1,127 +0,0 @@ -/* @flow */ -'use strict'; - -import HDKey from 'hdkey'; -import EthereumjsUtil from 'ethereumjs-util'; - -import * as ACTIONS from '../actions'; -import BigNumber from 'bignumber.js'; -import { access, stat } from 'fs'; - -type AddressType = { - devicePath: string; - index: number; - path: Array; - address: string; -} - -type State = { - addresses: Array
    ; - pendingTxs: Array; -} - -const initialState: State = { - addresses: [], - pendingTxs: [], -}; - -export class Address { - - devicePath: string; - index: number; - path: Array; - address: string; - balance: string; - pendingTx: Array; - history: JSON; - - constructor(devicePath: string, index: number, path: Array, address: string) { - this.devicePath = devicePath; - this.index = index; - this.path = path; - this.address = address; - this.pendingTx = []; - } - - setBalance(balance: string): void { - this.balance = balance; - } - - setHistory(json): void { - this.history = json; - } - - addPendingTx(txid, txData): void { - this.pendingTx.push({ - txid, - txData - }); - } - - findPendingTx(txid): boolean { - let index = this.pendingTx.findIndex(tx => tx.txid === txid); - return index >= 0; - } - - removePendingTx(txid): void { - let index = this.pendingTx.findIndex(tx => tx.txid === txid); - if (index >= 0) { - this.pendingTx.splice(index, 1); - console.error("-----> RMEOVE PENDiNG TX", this.pendingTx) - } - } -} - -export default (state: State = initialState, action: any): State => { - - switch (action.type) { - - case ACTIONS.ON_TX_COMPLETE : - action.address.addPendingTx(action.txid, action.txData); - state.pendingTxs.push(action.txid); - return { - ...state, - } - - case ACTIONS.TX_STATUS_OK : - for (let addr of state.addresses) { - addr.removePendingTx(action.txid); - } - let pendingIndex = state.pendingTxs.indexOf(action.txid); - if (pendingIndex >= 0) { - state.pendingTxs.splice(pendingIndex, 1); - } - return { - ...state, - } - - break; - - case ACTIONS.ADDRESS_CREATE : - return { - ...state, - addresses: state.addresses.concat([ action.address ]) - } - - case ACTIONS.ADDRESS_SET_BALANCE : - action.address.setBalance( action.balance ); - return { - ...state, - } - - case ACTIONS.ADDRESS_SET_HISTORY : - action.address.setHistory( action.history ); - return { - ...state, - } - - case ACTIONS.ADDRESS_DELETE : - return { - ...state, - addresses: action.addresses - } - default: - return state; - } - -} \ No newline at end of file diff --git a/src/js/reducers/AppReducer.js b/src/js/reducers/AppReducer.js new file mode 100644 index 00000000..a033b40a --- /dev/null +++ b/src/js/reducers/AppReducer.js @@ -0,0 +1,49 @@ +/* @flow */ +'use strict'; + +import { ON_RESIZE, TOGGLE_DEVICE_DROPDOWN, RESIZE_CONTAINER } from '../actions/AppActions'; +import * as WEB3 from '../actions/constants/Web3'; + +const WIDTH: number = 1080; +const HEIGHT: number = 1920; + +const initialState: Object = { + orginalWidth: WIDTH, + orginalHeight: HEIGHT, + width: window.innerWidth, + height: window.innerHeight, + scale: Math.min(window.innerWidth / WIDTH, window.innerHeight / HEIGHT), + coinDropdownOpened: false, + deviceDropdownOpened: false, + initialized: false, + landingPage: true, +}; + +export default function DOM(state: Object = initialState, action: Object): any { + switch(action.type) { + case ON_RESIZE : + return { + ...state, + scale: Math.min(window.innerWidth / WIDTH, window.innerHeight / HEIGHT), + } + case RESIZE_CONTAINER : + return { + ...state, + coinDropdownOpened: action.opened + } + case TOGGLE_DEVICE_DROPDOWN : + return { + ...state, + deviceDropdownOpened: action.opened + } + + case WEB3.READY : + return { + ...state, + initialized: true + } + + default: + return state; + } +} diff --git a/src/js/reducers/DOMReducer.js b/src/js/reducers/DOMReducer.js deleted file mode 100644 index 4c317991..00000000 --- a/src/js/reducers/DOMReducer.js +++ /dev/null @@ -1,27 +0,0 @@ -/* @flow */ -'use strict'; - -import { ON_RESIZE } from '../actions/DOMActions'; - -const WIDTH: number = 1080; -const HEIGHT: number = 1920; - -const initialState: Object = { - orginalWidth: WIDTH, - orginalHeight: HEIGHT, - width: window.innerWidth, - height: window.innerHeight, - scale: Math.min(window.innerWidth / WIDTH, window.innerHeight / HEIGHT), -}; - -export default function DOM(state: Object = initialState, action: Object): void { - switch(action.type) { - case ON_RESIZE : - return { - ...state, - scale: Math.min(window.innerWidth / WIDTH, window.innerHeight / HEIGHT), - } - default: - return state; - } -} diff --git a/src/js/reducers/DiscoveryReducer.js b/src/js/reducers/DiscoveryReducer.js new file mode 100644 index 00000000..446830ed --- /dev/null +++ b/src/js/reducers/DiscoveryReducer.js @@ -0,0 +1,138 @@ +/* @flow */ +'use strict'; + +import * as DISCOVERY from '../actions/constants/Discovery'; +import * as ADDRESS from '../actions/constants/Address'; +import * as CONNECT from '../actions/constants/TrezorConnect'; + +export type Discovery = { + coin: string; + checksum: string; + xpub: string; + accountIndex: number; + interrupted: boolean; + completed: boolean; + waitingForDevice: boolean; + waitingForAuth?: boolean; +} + +const initialState: Array = []; + +const start = (state: Array, action: any): Array => { + + const instance: Discovery = { + coin: action.coin, + xpub: action.xpub, + hdKey: action.hdKey, + basePath: action.basePath, + checksum: action.device.checksum, + accountIndex: 0, + interrupted: false, + completed: false, + waitingForDevice: false + } + + const newState: Array = [ ...state ]; + const index: number = state.findIndex(d => { + return d.coin === action.coin && d.checksum === action.device.checksum; + }); + + console.warn("START DISCO", index); + + if (index >= 0) { + newState[index] = instance; + } else { + newState.push(instance); + } + return newState; +} + +const complete = (state: Array, action: any): Array => { + const index: number = state.findIndex(d => { + return d.coin === action.coin && d.checksum === action.device.checksum; + }); + const newState: Array = [ ...state ]; + newState[index].completed = true; + return newState; +} + +const addressCreate = (state: Array, action: any): Array => { + const index: number = state.findIndex(d => { + return d.coin === action.coin && d.checksum === action.device.checksum; + }); + const newState: Array = [ ...state ]; + newState[index].accountIndex++; + return newState; +} + +const forgetDiscovery = (state: Array, action: any): Array => { + return state.filter(d => d.checksum !== action.device.checksum); +} + +const stop = (state: Array, action: any): Array => { + const newState: Array = [ ...state ]; + return newState.map( (d: Discovery) => { + if (d.checksum === action.device.checksum && !d.completed) { + d.interrupted = true; + d.waitingForDevice = false; + } + return d; + }); +} + +const waiting = (state: Array, action: any): Array => { + + const instance: Discovery = { + coin: action.coin, + checksum: action.device.checksum, + xpub: '', + accountIndex: 0, + interrupted: false, + completed: false, + waitingForDevice: true + } + + const index: number = state.findIndex(d => { + return d.coin === action.coin && d.checksum === action.device.checksum; + }); + + const newState: Array = [ ...state ]; + if (index >= 0) { + newState[index] = instance; + } else { + newState.push(instance); + } + + return newState; +} + +export default function discovery(state: Array = initialState, action: any): any { + + switch (action.type) { + case DISCOVERY.START : + return start(state, action); + case ADDRESS.CREATE : + return addressCreate(state, action); + case DISCOVERY.STOP : + return stop(state, action); + case DISCOVERY.COMPLETE : + return complete(state, action); + case DISCOVERY.WAITING : + return waiting(state, action) + case DISCOVERY.FROM_STORAGE : + return action.payload.map(d => { + return { + ...d, + interrupted: false, + waitingForDevice: false + } + }) + case CONNECT.FORGET : + case CONNECT.FORGET_SINGLE : + return forgetDiscovery(state, action); + + default: + return state; + } + +} \ No newline at end of file diff --git a/src/js/reducers/LocalStorageReducer.js b/src/js/reducers/LocalStorageReducer.js new file mode 100644 index 00000000..4e1e2481 --- /dev/null +++ b/src/js/reducers/LocalStorageReducer.js @@ -0,0 +1,47 @@ +/* @flow */ +'use strict'; + +import * as STORAGE from '../actions/constants/LocalStorage'; + +type State = { + initialized: boolean; + error: any; + config: any; + ethERC20: any; + ethTokens: any; +} + +const initialState: State = { + initialized: false, + error: null, + config: null, + ethERC20: null, + ethTokens: null, +}; + +export default function localStorage(state: State = initialState, action: any): any { + + switch (action.type) { + + case STORAGE.READY : + return { + ...state, + initialized: true, + config: action.appConfig, + ethERC20: action.ethERC20, + ethTokens: action.ethTokens, + error: null + } + + case STORAGE.ERROR : + return { + ...state, + error: action.error + } + + + default: + return state; + } + +} \ No newline at end of file diff --git a/src/js/reducers/ModalReducer.js b/src/js/reducers/ModalReducer.js index b84e572f..c4a35633 100644 --- a/src/js/reducers/ModalReducer.js +++ b/src/js/reducers/ModalReducer.js @@ -3,116 +3,105 @@ import { UI, DEVICE } from 'trezor-connect'; import * as ACTIONS from '../actions'; +import * as RECEIVE from '../actions/constants/receive'; +import * as MODAL from '../actions/constants/Modal'; +import * as CONNECT from '../actions/constants/TrezorConnect'; type ModalState = { opened: boolean; + device: any; windowType: ?string; - pin: string; - passphrase: string; - passphraseFocused: boolean; - passphraseVisible: boolean; - passphraseCached: boolean; - confirmation: ?string; } const initialState: ModalState = { opened: false, - windowType: null, - pin: "", - passphrase: "", - passphraseFocused: false, - passphraseVisible: false, - passphraseCached: true, - confirmation: null, + device: null, + windowType: null }; export default function modal(state: ModalState = initialState, action: any): any { switch (action.type) { - case UI.REQUEST_PIN : - case UI.INVALID_PIN : - case UI.REQUEST_PASSPHRASE : + case RECEIVE.REQUEST_UNVERIFIED : return { ...state, opened: true, windowType: action.type - }; + } - case UI.REQUEST_CONFIRMATION : + case CONNECT.REMEMBER_REQUEST : + case CONNECT.FORGET_REQUEST : + case CONNECT.DISCONNECT_REQUEST : return { ...state, + device: action.device, opened: true, - confirmation: action.data.label, windowType: action.type }; - case UI.REQUEST_PERMISSION : + case CONNECT.TRY_TO_DUPLICATE : return { ...state, + device: action.device, opened: true, - confirmation: action.data.label, windowType: action.type }; - case ACTIONS.CLOSE_MODAL : - return { - ...initialState, - passphraseCached: state.passphraseCached - }; - + case DEVICE.CHANGED : + if (state.opened && state.device && action.device.path === state.device.path && action.device.isUsedElsewhere) { + return { + ...initialState, + }; + } + return state; - case ACTIONS.ON_PIN_ADD : - let pin: string = state.pin; - if (pin.length < 9) { - pin += action.number; + case DEVICE.DISCONNECT : + if (state.device && action.device.path === state.device.path) { + return { + ...initialState, + } } - return { - ...state, - pin: pin, - }; - case ACTIONS.ON_PIN_BACKSPACE : - return { - ...state, - pin: state.pin.substring(0, state.pin.length - 1), - }; + return state; + // case DEVICE.CONNECT : + // case DEVICE.CONNECT_UNACQUIRED : + // if (state.opened && state.windowType === CONNECT.TRY_TO_FORGET) { + // return { + // ...initialState, + // passphraseCached: state.passphraseCached + // } + // } + // return state; - case ACTIONS.ON_PASSPHRASE_CHANGE : - return { - ...state, - passphrase: action.value - } - case ACTIONS.ON_PASSPHRASE_SHOW : - return { - ...state, - passphraseVisible: true - } - case ACTIONS.ON_PASSPHRASE_HIDE : - return { - ...state, - passphraseVisible: false - } - case ACTIONS.ON_PASSPHRASE_SAVE : - return { - ...state, - passphraseCached: true - } - case ACTIONS.ON_PASSPHRASE_FORGET : + case UI.REQUEST_PIN : + case UI.INVALID_PIN : + case UI.REQUEST_PASSPHRASE : return { ...state, - passphraseCached: false - } - case ACTIONS.ON_PASSPHRASE_FOCUS : + device: action.data.device, + opened: true, + windowType: action.type + }; + + case UI.REQUEST_BUTTON : return { ...state, - passphraseFocused: true + opened: true, + windowType: action.data.code } - case ACTIONS.ON_PASSPHRASE_BLUR : + + case UI.CLOSE_UI_WINDOW : + case ACTIONS.CLOSE_MODAL : + + case CONNECT.FORGET : + case CONNECT.FORGET_SINGLE : + case CONNECT.REMEMBER : return { - ...state, - passphraseFocused: false - } + ...initialState, + }; + + default: return state; diff --git a/src/js/reducers/NotificationReducer.js b/src/js/reducers/NotificationReducer.js new file mode 100644 index 00000000..b0edf133 --- /dev/null +++ b/src/js/reducers/NotificationReducer.js @@ -0,0 +1,68 @@ +/* @flow */ +'use strict'; + +import { LOCATION_CHANGE } from 'react-router-redux'; +import * as NOTIFICATION from '../actions/constants/notification'; + +type NotificationAction = { + label: string; + callback: any; +} + +type NotificationEntry = { + +id: ?string; + +type: string; + +title: string; + +message: string; + +cancelable: boolean; + +actions: Array; +} + +const initialState: Array = [ + // { + // id: undefined, + // type: "info", + // title: "Some static notification", + // message: "This one is not cancelable", + // cancelable: false, + // actions: [] + // } +]; + +const addNotification = (state: Array, payload: any): Array => { + const newState: Array = state.filter(e => !e.cancelable); + newState.push({ + id: payload.id, + type: payload.type, + title: payload.title.toString(), + message: payload.message.toString(), + cancelable: payload.cancelable, + actions: payload.actions + }); + + // TODO: sort + return newState; +} + +const closeNotification = (state: Array, payload: any): Array => { + if (payload && typeof payload.id === 'string') { + return state.filter(e => e.id !== payload.id); + } else { + return state.filter(e => !e.cancelable); + } +} + +export default function notification(state: Array = initialState, action: Object): Array { + switch(action.type) { + + case NOTIFICATION.ADD : + return addNotification(state, action.payload); + + case LOCATION_CHANGE : + case NOTIFICATION.CLOSE : + return closeNotification(state, action.payload); + + default: + return state; + } +} diff --git a/src/js/reducers/ReceiveReducer.js b/src/js/reducers/ReceiveReducer.js new file mode 100644 index 00000000..40819ed5 --- /dev/null +++ b/src/js/reducers/ReceiveReducer.js @@ -0,0 +1,51 @@ +/* @flow */ +'use strict'; + +import * as RECEIVE from '../actions/constants/receive'; + +export type State = { + +checksum: ?string; + +accountIndex: ?number; + +coin: ?string; + location: string; + addressVerified: boolean; + adressUnverified: boolean; +} + +export const initialState: State = { + checksum: null, + accountIndex: null, + coin: null, + location: '', + addressVerified: false, + addressUnverified: false, +}; + +export default (state: State = initialState, action: any): State => { + + switch (action.type) { + + case RECEIVE.INIT : + return action.state; + + case RECEIVE.DISPOSE : + return initialState; + + case RECEIVE.SHOW_ADDRESS : + return { + ...state, + addressVerified: true, + addressUnverified: false + } + case RECEIVE.SHOW_UNVERIFIED_ADDRESS : + return { + ...state, + addressVerified: false, + addressUnverified: true + } + + default: + return state; + } + +} \ No newline at end of file diff --git a/src/js/reducers/SendFormReducer.1.js b/src/js/reducers/SendFormReducer.1.js new file mode 100644 index 00000000..693d27c3 --- /dev/null +++ b/src/js/reducers/SendFormReducer.1.js @@ -0,0 +1,199 @@ +/* @flow */ +'use strict'; + +import { LOCATION_CHANGE } from 'react-router-redux'; +import * as SEND from '../actions/constants/SendForm'; +import * as WEB3 from '../actions/constants/Web3'; +import * as ADDRESS from '../actions/constants/Address'; +import EthereumjsUnits from 'ethereumjs-units'; +import BigNumber from 'bignumber.js'; +import { getFeeLevels } from '../actions/SendFormActions'; + +export type State = { + +senderAddress: ?string; + +coin: string; + token: string; + balance: string; + tokenBalance: string; + balanceNeedUpdate: boolean; + + + // form fields + advanced: boolean; + untouched: boolean; // set to true when user made some changes in form + touched: {[k: string]: boolean}; + address: string; + amount: string; + setMax: boolean; + feeLevels: Array; + selectedFeeLevel: ?FeeLevel; + recommendedGasPrice: string; + gasPriceNeedsUpdate: boolean; + gasLimit: string; + gasPrice: string; + data: string; + nonce: string; + total: string; + sending: boolean; + sendingStatus: ?SendStatus; + errors: {[k: string]: string}; + warnings: {[k: string]: string}; + infos: {[k: string]: string}; +} + +export type FeeLevel = { + label: string; + gasPrice: string; + value: string; +} + +type SendStatus = { + success: boolean; + message: string; +} + +export const initialState: State = { + senderAddress: null, + coin: '', + token: '', + advanced: false, + untouched: true, + touched: {}, + balance: '0', + tokenBalance: '0', + balanceNeedUpdate: false, + //address: '', + address: '0x574BbB36871bA6b78E27f4B4dCFb76eA0091880B', + amount: '', + setMax: false, + feeLevels: [], + selectedFeeLevel: null, + recommendedGasPrice: '0', + gasPriceNeedsUpdate: false, + gasLimit: '0', + gasPrice: '0', + data: '', + nonce: '0', + total: '0', + sending: false, + sendingStatus: null, + errors: {}, + warnings: {}, + infos: {}, +} + + +const onGasPriceUpdated = (state: State, action: any): State => { + + function getRandomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; + } + const newPrice = getRandomInt(10, 50).toString(); + //const newPrice = EthereumjsUnits.convert(action.gasPrice, 'wei', 'gwei'); + if (action.coin === state.coin && newPrice !== state.recommendedGasPrice) { + const newState: State = { ...state }; + if (!state.untouched) { + newState.gasPriceNeedsUpdate = true; + newState.recommendedGasPrice = newPrice; + } else { + const newFeeLevels = getFeeLevels(state.coin, newPrice, state.gasLimit); + const selectedFeeLevel = newFeeLevels.find(f => f.value === 'Normal'); + newState.recommendedGasPrice = newPrice; + newState.feeLevels = newFeeLevels; + newState.selectedFeeLevel = selectedFeeLevel; + newState.gasPrice = selectedFeeLevel.gasPrice; + } + return newState; + } + return state; +} + +const onBalanceUpdated = (state: State, action: any): State => { + // balanceNeedUpdate + if (state.senderAddress === action.address) { + return { + ...state, + balance: '1' + } + } + return state; +} + + +export default (state: State = initialState, action: any): State => { + + switch (action.type) { + + case SEND.INIT : + return action.state; + + case SEND.DISPOSE : + return initialState; + + // this will be called right after Web3 instance initialization before any view is shown + // and async during app live time + case WEB3.GAS_PRICE_UPDATED : + return onGasPriceUpdated(state, action); + + case ADDRESS.SET_BALANCE : + return onBalanceUpdated(state, action); + + case SEND.TOGGLE_ADVANCED : + return { + ...state, + advanced: !state.advanced + } + + + // user actions + case SEND.ADDRESS_CHANGE : + case SEND.AMOUNT_CHANGE : + case SEND.SET_MAX : + case SEND.CURRENCY_CHANGE : + case SEND.FEE_LEVEL_CHANGE : + case SEND.UPDATE_FEE_LEVELS : + case SEND.GAS_PRICE_CHANGE : + case SEND.GAS_LIMIT_CHANGE : + case SEND.DATA_CHANGE : + return action.state; + + case SEND.SEND : + return { + ...state, + sending: true, + sendingStatus: null, + } + + case SEND.TX_COMPLETE : + return { + ...state, + sending: false, + sendingStatus: { + success: true, + message: action.txid + } + } + case SEND.TX_ERROR : + return { + ...state, + sending: false, + sendingStatus: { + success: false, + message: action.response + } + } + + + case SEND.VALIDATION : + return { + ...state, + errors: action.errors, + warnings: action.warnings, + infos: action.infos, + } + + default: + return state; + } + +} \ No newline at end of file diff --git a/src/js/reducers/SendFormReducer.js b/src/js/reducers/SendFormReducer.js index 2ef1799b..f65dc313 100644 --- a/src/js/reducers/SendFormReducer.js +++ b/src/js/reducers/SendFormReducer.js @@ -1,71 +1,200 @@ /* @flow */ 'use strict'; -import * as ACTIONS from '../actions/index'; +import { LOCATION_CHANGE } from 'react-router-redux'; +import * as SEND from '../actions/constants/SendForm'; +import * as WEB3 from '../actions/constants/Web3'; +import * as ADDRESS from '../actions/constants/Address'; +import EthereumjsUnits from 'ethereumjs-units'; +import BigNumber from 'bignumber.js'; +import { getFeeLevels } from '../actions/SendFormActions'; -type State = { +export type State = { + +checksum: ?string; + +accountIndex: number; + +coin: string; + token: string; + location: string; + + balanceNeedUpdate: boolean; + + + // form fields + advanced: boolean; + untouched: boolean; // set to true when user made any changes in form + touched: {[k: string]: boolean}; address: string; - amount: number; - gasPrice: number; - gasLimit: number; + amount: string; + setMax: boolean; + feeLevels: Array; + selectedFeeLevel: ?FeeLevel; + recommendedGasPrice: string; + gasPriceNeedsUpdate: boolean; + gasLimit: string; + gasPrice: string; data: string; + nonce: string; + total: string; + sending: boolean; + sendingStatus: ?SendStatus; + errors: {[k: string]: string}; + warnings: {[k: string]: string}; + infos: {[k: string]: string}; +} + +export type FeeLevel = { + label: string; + gasPrice: string; + value: string; +} + +type SendStatus = { + success: boolean; + message: string; +} + +export const initialState: State = { + checksum: null, + accountIndex: 0, + coin: '', + token: '', + location: '', + + advanced: false, + untouched: true, + touched: {}, + balanceNeedUpdate: false, + //address: '', + address: '0x574BbB36871bA6b78E27f4B4dCFb76eA0091880B', + amount: '', + setMax: false, + feeLevels: [], + selectedFeeLevel: null, + recommendedGasPrice: '0', + gasPriceNeedsUpdate: false, + gasLimit: '0', + gasPrice: '0', + data: '', + nonce: '0', + total: '0', + sending: false, + sendingStatus: null, + errors: {}, + warnings: {}, + infos: {}, +} + + +const onGasPriceUpdated = (state: State, action: any): State => { + + function getRandomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; + } + const newPrice = getRandomInt(10, 50).toString(); + //const newPrice = EthereumjsUnits.convert(action.gasPrice, 'wei', 'gwei'); + if (action.coin === state.coin && newPrice !== state.recommendedGasPrice) { + const newState: State = { ...state }; + if (!state.untouched) { + newState.gasPriceNeedsUpdate = true; + newState.recommendedGasPrice = newPrice; + } else { + const newFeeLevels = getFeeLevels(state.coin, newPrice, state.gasLimit); + const selectedFeeLevel = newFeeLevels.find(f => f.value === 'Normal'); + newState.recommendedGasPrice = newPrice; + newState.feeLevels = newFeeLevels; + newState.selectedFeeLevel = selectedFeeLevel; + newState.gasPrice = selectedFeeLevel.gasPrice; + } + return newState; + } + return state; +} + +const onBalanceUpdated = (state: State, action: any): State => { + // balanceNeedUpdate + // if (state.senderAddress === action.address) { + // return { + // ...state, + // balance: '1' + // } + // } + + // TODO: handle balance update during send form lifecycle + return state; } -const initialState: State = { - //address: '0x7314e0f1C0e28474bDb6be3E2c3E0453255188f8', //metamask acc1 - address: '0xa738ea40b69d87f4f9ac94c9a0763f96248df23b', // trezor acc3 - amount: 0.0001, - gasPrice: 0, - gasPriceChanged: false, - gasLimit: 21000, - data: '' -}; export default (state: State = initialState, action: any): State => { switch (action.type) { - case 'update_gas' : - if (!state.gasPriceChanged) { - return { - ...state, - gasPrice: action.gasPrice - } - } - return { - ...state, - } - - - case ACTIONS.ON_ADDRESS_CHANGE : + case SEND.INIT : + return action.state; + + case SEND.DISPOSE : + return initialState; + + // this will be called right after Web3 instance initialization before any view is shown + // and async during app live time + case WEB3.GAS_PRICE_UPDATED : + return onGasPriceUpdated(state, action); + + case ADDRESS.SET_BALANCE : + // case ADDRESS.SET_TOKEN_BALANCE : + return onBalanceUpdated(state, action); + + case SEND.TOGGLE_ADVANCED : return { ...state, - address: action.address + advanced: !state.advanced } - case ACTIONS.ON_AMOUNT_CHANGE : + + // user actions + case SEND.ADDRESS_CHANGE : + case SEND.AMOUNT_CHANGE : + case SEND.SET_MAX : + case SEND.CURRENCY_CHANGE : + case SEND.FEE_LEVEL_CHANGE : + case SEND.UPDATE_FEE_LEVELS : + case SEND.GAS_PRICE_CHANGE : + case SEND.GAS_LIMIT_CHANGE : + case SEND.DATA_CHANGE : + return action.state; + + case SEND.SEND : return { ...state, - amount: action.amount + sending: true, + sendingStatus: null, } - case ACTIONS.ON_GAS_PRICE_CHANGE : + case SEND.TX_COMPLETE : return { ...state, - gasPriceChanged: true, - gasPrice: action.gasPrice + sending: false, + sendingStatus: { + success: true, + message: action.txid + } } - - case ACTIONS.ON_GAS_LIMIT_CHANGE : + case SEND.TX_ERROR : return { ...state, - gasLimit: action.gasLimit + sending: false, + sendingStatus: { + success: false, + message: action.response + } } - case ACTIONS.ON_TX_DATA_CHANGE : + + case SEND.VALIDATION : return { ...state, - data: action.data + errors: action.errors, + warnings: action.warnings, + infos: action.infos, } default: diff --git a/src/js/reducers/SummaryReducer.js b/src/js/reducers/SummaryReducer.js new file mode 100644 index 00000000..41a59499 --- /dev/null +++ b/src/js/reducers/SummaryReducer.js @@ -0,0 +1,47 @@ +/* @flow */ +'use strict'; + +import * as SUMMARY from '../actions/constants/summary'; + +export type State = { + +checksum: ?string; + +accountIndex: ?number; + +coin: ?string; + location: string; + + details: boolean; + selectedToken: any; +} + +export const initialState: State = { + checksum: null, + accountIndex: null, + coin: null, + location: '', + + details: true, + selectedToken: null +}; + + +export default (state: State = initialState, action: any): State => { + + switch (action.type) { + + case SUMMARY.INIT : + return action.state; + + case SUMMARY.DISPOSE : + return initialState; + + case SUMMARY.DETAILS_TOGGLE : + return { + ...state, + details: !state.details + } + + default: + return state; + } + +} \ No newline at end of file diff --git a/src/js/reducers/TokensReducer.js b/src/js/reducers/TokensReducer.js new file mode 100644 index 00000000..68feb7f7 --- /dev/null +++ b/src/js/reducers/TokensReducer.js @@ -0,0 +1,71 @@ +/* @flow */ +'use strict'; + +import * as CONNECT from '../actions/constants/TrezorConnect'; +import * as TOKEN from '../actions/constants/Token'; + +export type Token = { + loaded: boolean; + +checksum: string; + +name: string; + +symbol: string; + +address: string; + +ethAddress: string; // foreign key + +decimals: string; + balance: string; +} + +const initialState: Array = []; + +const setBalance = (state: Array, payload: any): Array => { + const newState: Array = [ ...state ]; + let index: number = state.findIndex(t => t.address === payload.address && t.ethAddress === payload.ethAddress); + if (index >= 0) { + newState[index].loaded = true; + newState[index].balance = payload.balance; + } + return newState; +} + +const create = (state: Array, payload: any): Array => { + const newState: Array = [ ...state ]; + const token: Token = { + loaded: false, + checksum: payload.checksum, + name: payload.name, + symbol: payload.symbol, + address: payload.address, + ethAddress: payload.ethAddress, + decimals: payload.decimals, + balance: '0' + } + newState.push(token); + return newState; +} + +const forget = (state: Array, action: any): Array => { + return state.filter(t => t.checksum !== action.device.checksum); +} + +export default (state: Array = initialState, action: any): Array => { + + switch (action.type) { + + case TOKEN.ADD : + return create(state, action.payload); + + case TOKEN.SET_BALANCE : + return setBalance(state, action.payload); + + case CONNECT.FORGET : + case CONNECT.FORGET_SINGLE : + return forget(state, action); + + case TOKEN.FROM_STORAGE : + return action.payload; + + default: + return state; + } + +} \ No newline at end of file diff --git a/src/js/reducers/TrezorConnectReducer.js b/src/js/reducers/TrezorConnectReducer.js index cd7a7837..cbae74f1 100644 --- a/src/js/reducers/TrezorConnectReducer.js +++ b/src/js/reducers/TrezorConnectReducer.js @@ -1,81 +1,361 @@ /* @flow */ 'use strict'; -import { DEVICE } from 'trezor-connect'; +import { TRANSPORT, DEVICE } from 'trezor-connect'; +import * as CONNECT from '../actions/constants/TrezorConnect'; + +export type TrezorDevice = { + initialized: boolean; + remember: boolean; + connected: boolean; + path: string; + +label: string; + +checksum: string; + +instance?: number; + instanceLabel: string; + features?: Object; + unacquired?: boolean; + acquiring: boolean; + isUsedElsewhere?: boolean; + featuresNeedsReload?: boolean; + ts: number; +} + +export type SelectedDevice = { + id: string; + instance: ?number; +} type State = { - devices: Array, - selectedDevice: ?string; + devices: Array; + selectedDevice: ?SelectedDevice; + discoveryComplete: boolean; + error: any; } + const initialState: State = { devices: [], - selectedDevice: undefined, + selectedDevice: null, + discoveryComplete: false, + error: null, }; -const findDeviceIndexByPath = (devices: Array, path: string): number => { - let index: number = -1; - for (let [i, dev] of devices.entries() ) { - if (dev.path === path) { - index = i; - break; +export const findSelectedDevice = (state: State): ?TrezorDevice => { + const selected: ?SelectedDevice = state.selectedDevice; + if (!selected) return null; + + return state.devices.find(d => { + if (d.unacquired && d.path === selected.id) { + return true; + } else if (d.features && d.features.device_id === selected.id && d.instance === selected.instance){ + return true; + } + return false; + }); +} + +export const isSavedDevice = (state: State, device: any): ?Array => { + const selected: ?SelectedDevice = state.selectedDevice; + if (!selected) return null; + + if (!device || !device.features) return null; + + return state.devices.filter(d => { + if (d.features && d.features.device_id === device.features.device_id){ + return d; + } + return null; + }); +} + +const mergeDevices = (current: TrezorDevice, upcoming: Object): TrezorDevice => { + const dev: TrezorDevice = { + // ...current, + ...upcoming, + // make sure that instance specific variables will not be overridden + initialized: current.initialized, + connected: typeof upcoming.connected === 'boolean' ? upcoming.connected : current.connected, + remember: typeof upcoming.remember === 'boolean' ? upcoming.remember : current.remember, + instance: current.instance, + instanceLabel: current.instanceLabel, + checksum: current.checksum, + acquiring: typeof upcoming.acquiring === 'boolean' ? upcoming.acquiring : current.acquiring, + ts: new Date().getTime(), + } + + if (upcoming.unacquired && current.checksum) { + dev.instanceLabel = current.instanceLabel; + dev.features = current.features; + dev.label = current.label; + dev.unacquired = false; + } else if (!upcoming.unacquired && current.unacquired) { + dev.instanceLabel = upcoming.label; + if (typeof dev.instance === 'number') { + dev.instanceLabel = `${upcoming.label} #${dev.instance}`; } } - return index; + + return dev; } const addDevice = (state: State, device: Object): State => { - let index: number = findDeviceIndexByPath(state.devices, device.path); - if (index > -1) { - state.devices[index] = device; + + const newState: State = { ...state }; + + let affectedDevices: Array = []; + let otherDevices: Array = []; + if (device.unacquired) { + // connected device is unacquired, but it was already merged with saved devices + // when DEVICE.CHANGE event occurs + // ignore this event + affectedDevices = newState.devices.filter(d => d.path === device.path); + const diff = newState.devices.filter(d => affectedDevices.indexOf(d) === -1); + + if (affectedDevices.length > 0) { + return state; + } } else { - state.devices.push(device); + affectedDevices = newState.devices.filter(d => d.features && d.features.device_id === device.features.device_id); + otherDevices = newState.devices.filter(d => d.features && d.features.device_id !== device.features.device_id); } - return state; -} -const removeDevice = (state: State, device: Object): State => { - if (state.selectedDevice === device.path) { - state.selectedDevice = undefined; + if (affectedDevices.length > 0 ) { + // replace existing values + const changedDevices: Array = affectedDevices.map(d => mergeDevices(d, { ...device, connected: true} )); + newState.devices = otherDevices.concat(changedDevices); + + } else { + + const newDevice: TrezorDevice = { + ...device, + initialized: false, + acquiring: false, + remember: false, + connected: true, + path: device.path, + label: device.label, + id: 'ABCD', + checksum: null, + // instance: 0, + instanceLabel: device.label, + ts: 0, + } + newState.devices.push(newDevice); + + // const clone = { ...newDevice, instance: 1, instanceLabel: device.label + '#1' }; + // newState.devices.push(clone); } - let index: number = findDeviceIndexByPath(state.devices, device.path); + + return newState; +} + +const setDeviceState = (state: State, action: any): State => { + const newState: State = { ...state }; + + //const affectedDevice: ?TrezorDevice = state.devices.find(d => d.path === action.device.path && d.instance === action.device.instance); + const index: number = state.devices.findIndex(d => d.path === action.device.path && d.instance === action.device.instance); if (index > -1) { + const changedDevice: TrezorDevice = { + ...newState.devices[index], + initialized: true, + checksum: action.checksum + }; + newState.devices[index] = changedDevice; + //newState.selectedDevice = changedDevice; + } + + return newState; +} + +const changeDevice = (state: State, device: Object): State => { + + const newState: State = { ...state }; + + let affectedDevices: Array = []; + let otherDevices: Array = []; + if (device.features) { + affectedDevices = state.devices.filter(d => (d.features && d.features.device_id === device.features.device_id) || (d.path.length > 0 && d.path === device.path) ); + otherDevices = state.devices.filter(d => affectedDevices.indexOf(d) === -1); + } else { + affectedDevices = state.devices.filter(d => d.path === device.path); + otherDevices = state.devices.filter(d => d.path !== device.path); + } + + if (affectedDevices.length > 0) { + + const isAffectedUnacquired: number = affectedDevices.findIndex(d => d.unacquired); + if (isAffectedUnacquired >= 0 && affectedDevices.length > 1){ + affectedDevices.splice(isAffectedUnacquired, 1); + } + + // else if (isAffectedUnacquired >= 0 && !device.unacquired && affectedDevices.length > 1) { + // affectedDevices.splice(isAffectedUnacquired, 1); + // console.warn("CLEARRRR", isAffectedUnacquired); + // } + console.warn("AFFEEE", isAffectedUnacquired, affectedDevices, otherDevices) + + + // acquiring selected device. remove unnecessary (not acquired) device from list + // after this action selectedDevice needs to be updated (in TrezorConnectService) + if (state.selectedDevice && device.path === state.selectedDevice.id && affectedDevices.length > 1) { + console.warn("clear dupli", affectedDevices, otherDevices) + // affectedDevices = affectedDevices.filter(d => d.path !== state.selectedDevice.id && d.features); + } + - state.devices.splice(index, 1); + + // replace existing values + const changedDevices: Array = affectedDevices.map(d => mergeDevices(d, device)); + newState.devices = otherDevices.concat(changedDevices); + } + + return newState; +} + + +const disconnectDevice = (state: State, device: Object): State => { + + const newState: State = { ...state }; + const affectedDevices: Array = state.devices.filter(d => d.path === device.path); + const otherDevices: Array = state.devices.filter(d => affectedDevices.indexOf(d) === -1); + + if (affectedDevices.length > 0) { + const acquiredDevices = affectedDevices.filter(d => !d.unacquired && d.checksum); + newState.devices = otherDevices.concat( acquiredDevices.map(d => { + d.connected = false; + d.isUsedElsewhere = false; + d.featuresNeedsReload = false; + d.acquiring = false; + //if (d.remember) + d.path = ''; + return d; + })); + } + + // selected device was removed and forgotten + // clear this field + const selected = findSelectedDevice(newState); + if (!selected) { + newState.selectedDevice = null; + } + + return newState; +} + +const forgetDevice = (state: State, action: any): State => { + const newState: State = { ...state }; + + if (action.type === CONNECT.FORGET_SINGLE) { + // remove only one instance (called from Aside button) + newState.devices.splice(newState.devices.indexOf(action.device), 1); + } else { + // remove all instances after disconnect (remember request declined) + newState.devices = state.devices.filter(d => d.path !== action.device.path); } - return state; + + return newState; } -const onDeviceStateChange = (device: Object): void => { +const devicesFromLocalStorage = (devices: Array): Array => { + return devices.map(d => { + return { + ...d, + connected: false, + path: '', + acquiring: false, + featuresNeedsReload: false, + isUsedElsewhere: false + } + }); +} + +const duplicate = (state: State, device: any): State => { + const newState: State = { ...state }; + const affectedDevices: Array = state.devices.filter(d => d.path === device.path); + + // if (affectedDevices.length > 0) { + const newDevice: TrezorDevice = { + ...device, + initialized: false, + checksum: null, + remember: device.remember, + connected: device.connected, + path: device.path, + label: device.label, + id: 'ABCD', + instance: new Date().getTime(), + instanceLabel: device.instanceLabel, + ts: 0, + } + newState.devices.push(newDevice); + newState.selectedDevice = { + id: newDevice.features.device_id, + instance: newDevice.instance + } + //} + return newState; } + + export default function connect(state: State = initialState, action: any): any { switch (action.type) { - case DEVICE.CONNECT : - case DEVICE.CONNECT_UNACQUIRED : + case CONNECT.DUPLICATE : + return duplicate(state, action.device); + + + case CONNECT.SELECT_DEVICE : return { ...state, - ...addDevice(state, action.device) - }; - break; + selectedDevice: action.payload + } - case DEVICE.DISCONNECT : - case DEVICE.DISCONNECT_UNACQUIRED : + case CONNECT.INITIALIZATION_ERROR : return { ...state, - ...removeDevice(state, action.device) + error: action.error }; - break; - case 'select_device' : + case TRANSPORT.ERROR : return { ...state, - selectedDevice: action.path, + error: action.device // message is wrapped in "device" field. It's dispatched from TrezorConnect.on(DEVICE_EVENT...) in TrezorConnectService }; - break; + + case CONNECT.DEVICE_FROM_STORAGE : + return { + ...state, + devices: devicesFromLocalStorage(action.payload), + } + + case CONNECT.AUTH_DEVICE : + return setDeviceState(state, action); + + case DEVICE.CONNECT : + case DEVICE.CONNECT_UNACQUIRED : + return addDevice(state, action.device); + + case CONNECT.REMEMBER : + return changeDevice(state, { ...action.device, path: '', remember: true } ); + + case CONNECT.FORGET : + case CONNECT.FORGET_SINGLE : + return forgetDevice(state, action); + + case CONNECT.START_ACQUIRING : + case CONNECT.STOP_ACQUIRING : + return changeDevice(state, { ...action.device, acquiring: action.type === CONNECT.START_ACQUIRING } ); + + case DEVICE.DISCONNECT : + case DEVICE.DISCONNECT_UNACQUIRED : + return disconnectDevice(state, action.device); + + case DEVICE.CHANGED : + return changeDevice(state, { ...action.device, connected: true }); default: return state; diff --git a/src/js/reducers/WalletReducer.js b/src/js/reducers/WalletReducer.js new file mode 100644 index 00000000..850a3a31 --- /dev/null +++ b/src/js/reducers/WalletReducer.js @@ -0,0 +1,27 @@ +/* @flow */ +'use strict'; + +import { ON_RESIZE, TOGGLE_DEVICE_DROPDOWN, RESIZE_CONTAINER } from '../actions/AppActions'; +import * as WEB3 from '../actions/constants/Web3'; + +const WIDTH: number = 1080; +const HEIGHT: number = 1920; + +type State = { + coin: string; + device: string; + +} + +const initialState: Object = { + +}; + +export default function wallet(state: Object = initialState, action: Object): any { + switch(action.type) { + + + default: + return state; + } +} diff --git a/src/js/reducers/Web3Reducer.1.js b/src/js/reducers/Web3Reducer.1.js new file mode 100644 index 00000000..a71f899d --- /dev/null +++ b/src/js/reducers/Web3Reducer.1.js @@ -0,0 +1,70 @@ +/* @flow */ +'use strict'; + +import Web3 from 'web3'; + +import { UI, DEVICE } from 'trezor-connect'; +import * as ACTIONS from '../actions'; +import * as STORAGE from '../actions/constants/LocalStorage'; +import * as WEB3 from '../actions/constants/Web3'; + +type State = { + web3: Web3; + url: Array; + customUrl: string; + tokens: JSON; + abi: JSON; + gasPrice: any; + latestBlock: any; + fiatRate: ?string; +} + +const initialState: State = { + web3: null, + url: [ + 'https://ropsten.infura.io/QGyVKozSUEh2YhL4s2G4', + 'https://api.myetherapi.com/rop', + 'https://pyrus2.ubiqscan.io', + ], + customUrl: 's', + gasPrice: 0, + latestBlock: 0, +}; + +export default function web3(state: State = initialState, action: any): any { + + switch (action.type) { + + case 'rate__update' : + return { + ...state, + fiatRate: action.rate.price_usd + } + + case STORAGE.READY : + return { + ...state, + tokens: action.tokens, + abi: action.abi + } + + case WEB3.READY : + return { + ...state, + web3: action.web3 + } + case WEB3.BLOCK_UPDATED : + return { + ...state, + latestBlock: action.blockHash + } + case WEB3.GAS_PRICE_UPDATED : + return { + ...state, + gasPrice: action.gasPrice + } + default: + return state; + } + +} \ No newline at end of file diff --git a/src/js/reducers/Web3Reducer.js b/src/js/reducers/Web3Reducer.js index 79bea4da..8d44d59d 100644 --- a/src/js/reducers/Web3Reducer.js +++ b/src/js/reducers/Web3Reducer.js @@ -1,26 +1,64 @@ /* @flow */ 'use strict'; -import { UI, DEVICE } from 'trezor-connect'; -import * as ACTIONS from '../actions'; +import Web3 from 'web3'; -type State = { - web3: any; +import * as STORAGE from '../actions/constants/LocalStorage'; +import * as WEB3 from '../actions/constants/Web3'; + +type Web3Instance = { + coin: string; + web3: Web3; + chainId: number; + latestBlock: any; + gasPrice: any; + erc20: any; +} + +const initialState: Array = []; + +const createWeb3 = (state: Array, action: any): Array => { + const instance: Web3Instance = { + coin: action.name, + web3: action.web3, + chainId: parseInt(action.chainId), + latestBlock: '0', + gasPrice: '0', + erc20: action.erc20 + } + const newState: Array = [ ...state ]; + newState.push(instance); + return newState; +} + +const updateLatestBlock = (state: Array, action: any): Array => { + const index: number = state.findIndex(w3 => { + return w3.coin === action.name; + }); + const newState: Array = [ ...state ]; + newState[index].latestBlock = action.blockHash; + return newState; } -const initialState: State = { - web3: null, -}; +const updateGasPrice = (state: Array, action: any): Array => { + const index: number = state.findIndex(w3 => { + return w3.coin === action.coin; + }); + const newState: Array = [ ...state ]; + newState[index].gasPrice = action.gasPrice; + return newState; +} -export default function web3(state: State = initialState, action: any): any { +export default function web3(state: Array = initialState, action: any): any { switch (action.type) { - case 'web3__init' : - return { - ...state, - web3: action.web3 - } + case WEB3.CREATE : + return createWeb3(state, action); + case WEB3.BLOCK_UPDATED : + return updateLatestBlock(state, action); + case WEB3.GAS_PRICE_UPDATED : + return updateGasPrice(state, action); default: return state; } diff --git a/src/js/reducers/index.js b/src/js/reducers/index.js index 299c4d01..11c06cd1 100644 --- a/src/js/reducers/index.js +++ b/src/js/reducers/index.js @@ -4,19 +4,33 @@ import { combineReducers } from 'redux'; import { routerReducer } from 'react-router-redux'; -import DOM from './DomReducer.js'; +import DOM from './AppReducer.js'; +import localStorage from './LocalStorageReducer.js'; import connect from './TrezorConnectReducer.js'; +import notifications from './NotificationReducer.js'; import modal from './ModalReducer.js'; import web3 from './Web3Reducer.js'; -import addresses from './AddressesReducer.js'; +import accounts from './AccountsReducer.js'; +import accountDetail from './AccountDetailReducer.js'; import sendForm from './SendFormReducer.js'; +import receive from './ReceiveReducer.js'; +import summary from './SummaryReducer.js'; +import tokens from './TokensReducer.js'; +import discovery from './DiscoveryReducer.js'; export default combineReducers({ router: routerReducer, DOM, + localStorage, connect, + notifications, modal, web3, - addresses, - sendForm + accounts, + accountDetail, + sendForm, + receive, + summary, + tokens, + discovery }); \ No newline at end of file diff --git a/src/js/router/index.js b/src/js/router/index.js index 197a8978..b2872867 100644 --- a/src/js/router/index.js +++ b/src/js/router/index.js @@ -2,20 +2,52 @@ 'use strict'; import React from 'react'; -import { Route } from 'react-router-dom'; +import { Route, Switch } from 'react-router-dom'; +import { Provider } from 'react-redux'; +import { ConnectedRouter } from 'react-router-redux'; +import store, { history } from '../store'; + import { - AppContainer, - LoadingContainer, + LandingPageContainer, + WalletContainer, + + AcquireContainer, + BootloaderContainer, + + DashboardContainer, + HistoryContainer, SendFormContainer, ReceiveContainer, + SignVerifyContainer, + SettingsContainer, } from '../containers'; +import SummaryContainer from '../components/wallet/summary/SummaryContainer'; + export default ( - - - - - - + + + + + + + + + + + + + + + + + {/* */} + + + + + + + ); \ No newline at end of file diff --git a/src/js/services/CoinmarketcapService.js b/src/js/services/CoinmarketcapService.js new file mode 100644 index 00000000..67047b09 --- /dev/null +++ b/src/js/services/CoinmarketcapService.js @@ -0,0 +1,39 @@ +/* @flow */ +'use strict'; + +import { LOCATION_CHANGE } from 'react-router-redux'; +import { httpRequest } from '../utils/networkUtils'; +import { resolveAfter } from '../utils/promiseUtils'; + +const loadRateAction = (): any => { + return async (dispatch, getState) => { + try { + const rate = await httpRequest('https://api.coinmarketcap.com/v1/ticker/ethereum/?convert=USD', 'json'); + + dispatch({ + type: 'rate__update', + rate: rate[0] + }) + + } catch(error) { + + } + + await resolveAfter(50000); + // dispatch( loadRateAction() ); + } +} + +/** + * Middleware + */ +const LocalStorageService = (store: any) => (next: any) => (action: any) => { + + if (action.type === LOCATION_CHANGE && !store.getState().router.location) { + store.dispatch(loadRateAction()); + } + + next(action); +}; + +export default LocalStorageService; \ No newline at end of file diff --git a/src/js/services/EtherscanService.js b/src/js/services/EtherscanService.js index 0b9fbadd..ae6d2862 100644 --- a/src/js/services/EtherscanService.js +++ b/src/js/services/EtherscanService.js @@ -1,4 +1,4 @@ -/* @flow */ +/* @flo */ 'use strict'; //http://ropsten.etherscan.io/api?module=account&action=txlist&address=0x98ead4bd2fbbb0cf0b49459aa0510ef53faa6cad&startblock=0&endblock=99999999&sort=asc&apikey=89IZG471H8ITVXY377I2QWJIT2D62DGG9Z @@ -25,8 +25,41 @@ const getTransactionStatus = async (txid: string): Promise> => { return json; } -export const loadTransactionStatus = (txid): Promise => { - return async (dispatch, getState) => { +const getTokenHistory = async (tokenAddress, address) => { + + // 0x58cda554935e4a1f2acbe15f8757400af275e084 + // 0x000000000000000000000000 + 98ead4bd2fbbb0cf0b49459aa0510ef53faa6cad + let url: string = 'https://ropsten.etherscan.io/api?module=logs&action=getLogs'; + url += '&fromBlock=0&toBlock=latest'; + url += `&address=${tokenAddress}`; + url += '&topic0=0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'; + url += `&topic2=${ address }`; + // https://api.etherscan.io/api?module=logs&action=getLogs + // &fromBlock=0 + // &toBlock=latest + // &address=[Token Contract Address] + // &topic0=0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef + // &topic1=[From Address, padded to 32 bytes - optional] + // &topic2=[To Address, padded to 32 bytes - optional] + + console.log("TOKENURL", url); + const json = await httpRequest(url); + return json; +} + +export const loadTokenHistory = (address): Promise => { + // https://ropsten.etherscan.io/apis#logs + return async (dispatch, getState): Promise => { + const tkn = '0x58cda554935e4a1f2acbe15f8757400af275e084'; + const ad = '0x00000000000000000000000098ead4bd2fbbb0cf0b49459aa0510ef53faa6cad'; + const incoming = await getTokenHistory(tkn, ad); + + console.log("TOKEN HIST!", JSON.parse(incoming) ); + } +} + +export const loadTransactionStatus = (txid): Promise => { + return async (dispatch, getState): Promise => { const json = await getTransactionStatus(txid); const status = JSON.parse(json); @@ -38,7 +71,23 @@ export const loadTransactionStatus = (txid): Promise => { for (let addr of addresses) { if (addr.findPendingTx(txid)) { dispatch( getBalance(addr) ); - dispatch( loadHistory(addr, 3000) ); + + const txType = status.result.from === addr.address ? 'out' : 'in'; + const txAddress = txType === 'out' ? status.result.to : status.result.from; + + dispatch({ + type: ACTIONS.ADDRESS_ADD_TO_HISTORY, + address: addr, + entry: { + txid: status.result.hash, + type: txType, + timestamp: '0', + address: txAddress, + value: status.result.value + } + }); + + //dispatch( loadHistory(addr, 3000) ); } } @@ -79,6 +128,7 @@ export const loadHistory = (address, delay): void => { // const json = await getTransactionStatus('0x2113e578497f3486944566e2417b5ac3b31d7e76f71557ae0626e2a6fe191e58'); // console.log("JSON!", json) + /* if (delay) { console.warn("-----PRELOAD with delay", address) await new Promise(resolve => { @@ -92,6 +142,7 @@ export const loadHistory = (address, delay): void => { address, history }); + */ } } @@ -110,7 +161,8 @@ const EtherscanService = store => next => action => { const addressId = parseInt(parts[2]); if (!isNaN(addressId) && addresses[addressId]) { - store.dispatch( loadHistory( addresses[addressId] ) ); + //store.dispatch( loadHistory( addresses[addressId] ) ); + //store.dispatch( loadTokenHistory( addresses[addressId] ) ); } //console.error("ETH", parts, "id", parts.length, parts.length === 3 && parts[1] === "address"); diff --git a/src/js/services/LocalStorageService.js b/src/js/services/LocalStorageService.js new file mode 100644 index 00000000..4cb8d915 --- /dev/null +++ b/src/js/services/LocalStorageService.js @@ -0,0 +1,111 @@ +/* @flow */ +'use strict'; + +import { LOCATION_CHANGE } from 'react-router-redux'; +import * as LocalStorageActions from '../actions/LocalStorageActions'; + +import { DEVICE } from 'trezor-connect'; +import * as CONNECT from '../actions/constants/TrezorConnect'; +import * as MODAL from '../actions/constants/Modal'; +import * as TOKEN from '../actions/constants/Token'; +import * as ADDRESS from '../actions/constants/Address'; +import * as DISCOVERY from '../actions/constants/Discovery'; + + +// https://github.com/STRML/react-localstorage/blob/master/react-localstorage.js +// or +// https://www.npmjs.com/package/redux-react-session + +const findAccounts = (devices, accounts) => { + return devices.reduce((arr, dev) => { + return arr.concat(accounts.filter(a => a.checksum === dev.checksum)); + }, []); +} + +const findTokens = (accounts, tokens) => { + return accounts.reduce((arr, account) => { + return arr.concat(tokens.filter(a => a.ethAddress === account.address)); + }, []); +} + +const findDiscovery = (devices, discovery) => { + return devices.reduce((arr, dev) => { + return arr.concat(discovery.filter(a => a.checksum === dev.checksum)); + }, []); +} + +const save = (dispatch, getState) => { + const devices = getState().connect.devices.filter(d => d.remember === true && !d.unacquired); + const accounts = findAccounts(devices, getState().accounts); + const tokens = findTokens(accounts, getState().tokens); + const discovery = findDiscovery(devices, getState().discovery); + + // save devices + dispatch( LocalStorageActions.save('devices', JSON.stringify(devices) ) ); + + // save already preloaded accounts + dispatch( LocalStorageActions.save('accounts', JSON.stringify(accounts) ) ); + + // save discovery state + dispatch( LocalStorageActions.save('discovery', JSON.stringify(discovery) ) ); + + // tokens + dispatch( LocalStorageActions.save('tokens', JSON.stringify( tokens ) ) ); +} + + +const LocalStorageService = (store: any) => (next: any) => (action: any) => { + + if (action.type === LOCATION_CHANGE) { + const { location } = store.getState().router; + if (!location) { + // load data from config.json and local storage + store.dispatch( LocalStorageActions.loadData() ); + } + } + + next(action); + + switch (action.type) { + + // first time saving + case CONNECT.REMEMBER : + save(store.dispatch, store.getState); + break; + + case TOKEN.ADD : + case TOKEN.SET_BALANCE : + save(store.dispatch, store.getState); + // store.dispatch( LocalStorageActions.save('tokens', JSON.stringify( tokens ) ) ); + break; + + case ADDRESS.CREATE : + case ADDRESS.SET_BALANCE : + case ADDRESS.SET_NONCE : + save(store.dispatch, store.getState); + //store.dispatch( LocalStorageActions.save('accounts', JSON.stringify( accounts ) ) ); + break; + + case DISCOVERY.START : + case DISCOVERY.STOP : + case DISCOVERY.COMPLETE : + // case DISCOVERY.WAITING : + save(store.dispatch, store.getState); + break; + + case CONNECT.FORGET : + case CONNECT.FORGET_SINGLE : + case DEVICE.CHANGED : + case DEVICE.DISCONNECT : + case CONNECT.AUTH_DEVICE : + save(store.dispatch, store.getState); + //store.dispatch( LocalStorageActions.save('devices', JSON.stringify( store.getState().connect.devices.filter(d => d.remember === true && !d.unacquired) ) ) ); + // store.dispatch( LocalStorageActions.save('selectedDevice', JSON.stringify( store.getState().connect.selectedDevice ) ) ); + break; + + } + + +}; + +export default LocalStorageService; \ No newline at end of file diff --git a/src/js/services/RouterService.js b/src/js/services/RouterService.js index 66bf7d23..2088e529 100644 --- a/src/js/services/RouterService.js +++ b/src/js/services/RouterService.js @@ -1,19 +1,155 @@ /* @flow */ 'use strict'; -import { LOCATION_CHANGE, push } from 'react-router-redux'; +import pathToRegexp from 'path-to-regexp'; +import { DEVICE } from 'trezor-connect'; +import { LOCATION_CHANGE, push, replace } from 'react-router-redux'; +import { ON_BEFORE_UNLOAD } from '../actions/AppActions'; +import * as CONNECT from '../actions/constants/TrezorConnect'; /** - * Middleware used for managing router path and - * checking if all required devices are online. - * It starts right before action is passed to reducers and add "alive" filed to every action - * which determining if current path is Adam or not + * Middleware used for init application and managing router path. */ -const RouterService = store => next => action => { - if (action.type === LOCATION_CHANGE) { + type UrlParams = {[k: string] : string}; + +const pathToParams = (path: string): UrlParams => { + const urlParts: Array = path.split("/").slice(1); + const params: UrlParams = {}; + if (urlParts.length < 1 || path === "/") return params; + + for (let i = 0, len = urlParts.length; i < len; i+=2) { + params[ urlParts[i] ] = urlParts[ i + 1 ]; + } + + if (params.hasOwnProperty('device')) { + const isClonedDevice: Array = params.device.split(':'); + if (isClonedDevice.length > 1) { + params.device = isClonedDevice[0]; + params.deviceInstance = parseInt(isClonedDevice[1]); + } + } + + return params; +} + +const validation = (store: any, params: UrlParams): boolean => { + + if (params.hasOwnProperty('device')) { + const { devices } = store.getState().connect; + + let device; // = devices.find(d => d.path === params.device || d.features.device_id === params.device); + if (params.hasOwnProperty('deviceInstance')) { + device = devices.find(d => d.features && d.features.device_id === params.device && d.instance === params.deviceInstance ); + } else { + device = devices.find(d => d.path === params.device || (d.features && d.features.device_id === params.device)); + } + + if (!device) return false; + } + + if (params.hasOwnProperty('coin')) { + const { config } = store.getState().localStorage; + const coin = config.coins.find(c => c.symbol === params.coin); + if (!coin) return false; + if (!params.address) return false; + } + + if (params.address) { + + } + + return true; +} + +let __unloading: boolean = false; + +const RouterService = (store: any) => (next: any) => (action: any) => { + + if (action.type === ON_BEFORE_UNLOAD) { + __unloading = true; + } else if (action.type === LOCATION_CHANGE && !__unloading) { + + const { location } = store.getState().router; + const web3 = store.getState().web3; + const { devices, error } = store.getState().connect; + const { opened } = store.getState().modal; + + let redirectPath: ?string; + // first (initial) event after app loads + if (!location) { + + action.payload.state = { + initURL: action.payload.pathname, + initSearch: action.payload.search + } + + // check if there are initial parameters in url (coin) + if (action.payload.search.length > 0) { + // save it in WalletReducer, after device detection will redirect to this request + redirectPath = '/'; + //action.payload.initURL = action.payload.location; + } + } + + const requestedParams: UrlParams = pathToParams(action.payload.pathname); + const currentParams: UrlParams = pathToParams(location ? location.pathname : '/'); + + // if web3 wasn't initialized yet or there are no devices attached or initialization error occurs + const landingPage: boolean = web3.length < 1 || devices.length < 1 || error; + + if (opened && action.payload.pathname !== location.pathname) { + redirectPath = location.pathname; + console.warn("Modal still opened"); + } else if (landingPage) { + // keep route on landing page + if (action.payload.pathname !== '/'){ + redirectPath = '/'; + } + } else { + // PATH VALIDATION + // redirect from root view + if (action.payload.pathname === '/' || !validation(store, requestedParams)) { + // TODO: switch to first device? + // redirectPath = `/device/${ devices[0].path }`; + redirectPath = location.pathname; + } else { + + if (currentParams.device !== requestedParams.device || currentParams.deviceInstance !== requestedParams.deviceInstance) { + store.dispatch({ + type: CONNECT.SELECT_DEVICE, + payload: { + id: requestedParams.device, + instance: requestedParams.deviceInstance + } + }); + } + + if (requestedParams.coin !== currentParams.coin) { + store.dispatch({ + type: CONNECT.COIN_CHANGED, + payload: { + coin: requestedParams.coin + } + }); + } + } + } + + if (redirectPath) { + console.warn("Redirecting...") + // override action to keep routerReducer sync + action.payload.params = pathToParams(redirectPath); + action.payload.pathname = redirectPath; + // change url + store.dispatch( replace(redirectPath) ); + } else { + action.payload.params = requestedParams; + } + } + // Pass all actions through by default next(action); }; diff --git a/src/js/services/TrezorConnectService.1.js b/src/js/services/TrezorConnectService.1.js new file mode 100644 index 00000000..e9267f1b --- /dev/null +++ b/src/js/services/TrezorConnectService.1.js @@ -0,0 +1,219 @@ +/* @flow */ +'use strict'; + +import { LOCATION_CHANGE, push } from 'react-router-redux'; + +import TrezorConnect, { TRANSPORT, DEVICE_EVENT, UI_EVENT, UI, DEVICE } from 'trezor-connect'; +import * as TrezorConnectActions from '../actions/TrezorConnectActions'; +import * as ModalActions from '../actions/ModalActions'; +import { init as initWeb3 } from '../actions/Web3Actions'; +import * as WEB3 from '../actions/constants/Web3'; +import * as STORAGE from '../actions/constants/LocalStorage'; +import * as CONNECT from '../actions/constants/TrezorConnect'; + + + +const initSelectedDevice = async (store: any, device: any): void => { + + const { selectedDevice } = store.getState().connect; + + console.log("WHATSUP?", device, selectedDevice) + + // if we are in LandingPage view switch it to Wallet + if (selectedDevice && selectedDevice.path === device.path && selectedDevice.instance === device.instance) { + if (selectedDevice.unacquired || selectedDevice.isUsedElsewhere) { + store.dispatch( push(`/device/${ selectedDevice.path }/acquire`) ); + } else { + if (device.features.bootloader_mode) { + store.dispatch( push(`/device/${ selectedDevice.path }/bootloader`) ); + } else { + + if (device.instance) { + store.dispatch( push(`/device/${ device.features.device_id }:${ device.instance }`) ); + } else { + store.dispatch( push(`/device/${ device.features.device_id }`) ); + } + + // if (!selectedDevice.initialized && selectedDevice.connected) { + // const response = await TrezorConnect.getPublicKey({ + // selectedDevice: selectedDevice.path, + // instance: selectedDevice.instance, + // path: "m/1'/0'/0'", + // confirmation: false + // }); + + // if (response && response.success) { + // const xpub = response.data.xpub; + // store.dispatch({ + // type: CONNECT.AUTH_DEVICE, + // device: selectedDevice, + // xpub + // }); + // } else { + // // TODO: error + // } + + // console.log("INIT SELECTED!", device, response) + // } + + + + + //store.dispatch( push(`/device/${ device.features.device_id }/coin/eth/address/0/send`) ); + //store.dispatch( push(`/device/${ device.features.device_id }/coin/eth/address/0`) ); + // store.dispatch( push(`/device/${ device.features.device_id }`) ); + + + + + + + // store.dispatch( TrezorConnectActions.startDiscoveryProcess(device) ); + + // get xpub to force + + } + } + } +} + +const TrezorConnectService = (store: any) => (next: any) => (action: any) => { + + if (action.type === DEVICE.DISCONNECT) { + const previous = store.getState().connect.selectedDevice; + next(action); + if (previous && action.device.path === previous.path) { + + if (previous.unacquired) { + + } else if (previous.initialized) { + // interrupt discovery process + store.dispatch( TrezorConnectActions.stopDiscoveryProcess(previous) ); + + if (!previous.remember) { + store.dispatch(ModalActions.askForRemember(previous)); + } + } + } + + return; + } + + if (action.type === DEVICE.ACQUIRED) { + const { selectedDevice } = store.getState().connect; + initSelectedDevice(store, selectedDevice); + } + + if (action.type === DEVICE.CHANGED) { + const previousSelectedDevice = store.getState().connect.selectedDevice; + // Pass actions BEFORE + next(action); + + if (previousSelectedDevice && action.device.path === previousSelectedDevice.path) { + //console.warn("TODO: Handle device changed, interrupt running async action (like discovery)", action.device); + } + } else if (action.type === DEVICE.DISCONNECT || action.type === CONNECT.SELECT_DEVICE) { + const previousSelectedDevice = store.getState().connect.selectedDevice; + // Pass actions BEFORE + next(action); + + + + const { devices, selectedDevice } = store.getState().connect; + if (!selectedDevice) { + store.dispatch( push('/') ); + } else if (previousSelectedDevice.path !== selectedDevice.path || previousSelectedDevice.instance !== selectedDevice.instance) { + + // interrupt discovery process + store.dispatch( TrezorConnectActions.stopDiscoveryProcess(previousSelectedDevice) ); + + initSelectedDevice(store, selectedDevice); + } + + } else if (action.type === TRANSPORT.ERROR) { + next(action); + store.dispatch( push('/') ); + } else { + // Pass all actions through by default + next(action); + } + + + + if (action.type === STORAGE.READY) { + + // TODO: check offline devices + + // set listeners + + TrezorConnect.on(DEVICE_EVENT, (event: DeviceMessage): void => { + // post event to TrezorConnectReducer + store.dispatch({ + type: event.type, + device: event.data + }); + }); + + const version: Object = TrezorConnect.getVersion(); + if (version.type === 'library') { + // handle UI events only if TrezorConnect isn't using popup + TrezorConnect.on(UI_EVENT, (type: string, data: any): void => { + // post event to reducers + store.dispatch({ + type, + data + }); + }); + } + + // init TrezorConnect library + + TrezorConnect.init({ + hostname: 'localhost', // TODO: controll it in Connect + transport_reconnect: false, + }) + .then(() => { + // post action inited + //store.dispatch({ type: 'WEB3_START' }); + + setTimeout(() => { + store.dispatch( initWeb3() ); + }, 2000) + + }) + .catch(error => { + store.dispatch({ + type: CONNECT.INITIALIZATION_ERROR, + error + }) + }); + + } else if (action.type === WEB3.READY) { + + const handleDeviceConnect = (device) => { + initSelectedDevice(store, device); + } + + const handleDeviceDisconnect = (device) => { + // remove addresses and discovery from state + store.dispatch( TrezorConnectActions.remove(device) ); + } + + TrezorConnect.on(DEVICE.CONNECT, handleDeviceConnect); + TrezorConnect.on(DEVICE.CONNECT_UNACQUIRED, handleDeviceConnect); + + TrezorConnect.on(DEVICE.DISCONNECT, handleDeviceDisconnect); + TrezorConnect.on(DEVICE.CONNECT_UNACQUIRED, handleDeviceDisconnect); + + // solve possible race condition: + // device was connected before Web3 initialized so we need to force DEVICE.CONNECT event on them + const { devices } = store.getState().connect; + for (let d of devices) { + handleDeviceConnect(d); + } + + } + +}; + +export default TrezorConnectService; \ No newline at end of file diff --git a/src/js/services/TrezorConnectService.js b/src/js/services/TrezorConnectService.js index a382a829..8776e6ba 100644 --- a/src/js/services/TrezorConnectService.js +++ b/src/js/services/TrezorConnectService.js @@ -1,65 +1,93 @@ /* @flow */ 'use strict'; -import { LOCATION_CHANGE } from 'react-router-redux'; +import { LOCATION_CHANGE, push } from 'react-router-redux'; -import TrezorConnect, { DEVICE_EVENT, UI_EVENT, UI, DEVICE } from 'trezor-connect'; +import TrezorConnect, { TRANSPORT, DEVICE_EVENT, UI_EVENT, UI, DEVICE } from 'trezor-connect'; import * as TrezorConnectActions from '../actions/TrezorConnectActions'; +import * as ModalActions from '../actions/ModalActions'; +import { init as initWeb3 } from '../actions/Web3Actions'; +import * as WEB3 from '../actions/constants/Web3'; +import * as STORAGE from '../actions/constants/LocalStorage'; +import * as CONNECT from '../actions/constants/TrezorConnect'; +import * as ACTIONS from '../actions'; -let inited: boolean = false; -const TrezorConnectService = store => next => action => { - // Pass all actions through by default +const TrezorConnectService = (store: any) => (next: any) => (action: any) => { + + const prevState = store.getState().connect; + const prevModalState = store.getState().connect; + next(action); - if (action.type === LOCATION_CHANGE && !inited) { - inited = true; + if (action.type === STORAGE.READY) { + store.dispatch( TrezorConnectActions.init() ); + + } else if (action.type === TRANSPORT.ERROR) { + store.dispatch( push('/') ); + + } else if (action.type === WEB3.READY) { + store.dispatch( TrezorConnectActions.postInit() ); - TrezorConnect.init() - .then(r => { - // post action inited - }) - .catch(error => { - // TODO: show some ui with errors - console.log("ERROR", error); - }); + } else if (action.type === DEVICE.DISCONNECT) { + store.dispatch( TrezorConnectActions.deviceDisconnect(action.device) ); - TrezorConnect.on(DEVICE_EVENT, (event: DeviceMessage): void => { - // post event to reducer + } else if (action.type === CONNECT.FORGET) { + //store.dispatch( TrezorConnectActions.forgetDevice(action.device) ); + store.dispatch( TrezorConnectActions.switchToFirstAvailableDevice() ); + } else if (action.type === CONNECT.FORGET_SINGLE) { + + //store.dispatch( TrezorConnectActions.forgetDevice(action.device) ); + + if (store.getState().connect.devices.length < 1 && action.device.connected) { + // prompt disconnect device modal store.dispatch({ - type: event.type, - device: event.data + type: CONNECT.DISCONNECT_REQUEST, + device: action.device }); - }); + } else { + store.dispatch( TrezorConnectActions.switchToFirstAvailableDevice() ); + } + } else if (action.type === DEVICE.CHANGED) { + // selected device was previously unacquired, but now it's acquired + // we need to change route + if (prevState.selectedDevice) { + if (!action.device.unacquired && action.device.path === prevState.selectedDevice.id) { + store.dispatch( TrezorConnectActions.onSelectDevice(action.device) ); + } + } + } else if (action.type === DEVICE.CONNECT || action.type === DEVICE.CONNECT_UNACQUIRED) { - const version: Object = TrezorConnect.getVersion(); + store.dispatch( TrezorConnectActions.restoreDiscovery() ); - if (version.type === 'library') { - // handle UI events only if TrezorConnect isn't using popup - TrezorConnect.on(UI_EVENT, (type: string, data: any): void => { - // post event to reducer + // interrupt process of remembering device (force forget) + // TODO: the same for disconnect more than 1 device at once + const { modal } = store.getState(); + if (modal.opened && modal.windowType === CONNECT.REMEMBER_REQUEST) { + if (action.device.features && modal.device.features.device_id === action.device.features.device_id) { store.dispatch({ - type, - data + type: ACTIONS.CLOSE_MODAL, }); - }); - } - - const handleDeviceConnect = (device) => { - store.dispatch( TrezorConnectActions.discover(device.path) ); + } else { + store.dispatch({ + type: CONNECT.FORGET, + device: modal.device + }); + } } - const handleDeviceDisconnect = (device) => { - store.dispatch( TrezorConnectActions.remove(device.path) ); - } + } else if (action.type === CONNECT.AUTH_DEVICE) { + store.dispatch( TrezorConnectActions.checkDiscoveryStatus() ); - TrezorConnect.on(DEVICE.CONNECT, handleDeviceConnect); - //TrezorConnect.on(DEVICE.CONNECT_UNACQUIRED, handleDeviceConnect); + } else if (action.type === CONNECT.DUPLICATE) { + store.dispatch( TrezorConnectActions.onDuplicateDevice() ); - TrezorConnect.on(DEVICE.DISCONNECT, handleDeviceDisconnect); - //TrezorConnect.on(DEVICE.CONNECT_UNACQUIRED, handleDeviceConnect); + } else if (action.type === DEVICE.ACQUIRED || action.type === CONNECT.SELECT_DEVICE) { + store.dispatch( TrezorConnectActions.getSelectedDeviceState() ); + } else if (action.type === CONNECT.COIN_CHANGED) { + store.dispatch( TrezorConnectActions.coinChanged( action.payload.coin ) ); } -}; +} export default TrezorConnectService; \ No newline at end of file diff --git a/src/js/services/Web3Service.js b/src/js/services/Web3Service.js index defb129b..c85fe007 100644 --- a/src/js/services/Web3Service.js +++ b/src/js/services/Web3Service.js @@ -4,102 +4,171 @@ import Web3 from 'web3'; import { LOCATION_CHANGE } from 'react-router-redux'; import * as ACTIONS from '../actions/index'; +import { getBalance, getGasPrice, getTransactionReceipt } from '../actions/Web3Actions'; import { loadTransactionStatus } from './EtherscanService'; import BigNumber from 'bignumber.js'; let web3: Web3; -// let pendingTxs: Array = []; - - -export const getGasPrice = (): Promise => { - return (dispatch, getState) => { - web3.eth.getGasPrice((error, gasPrice) => { - if (!error) { - dispatch({ - type: 'update_gas', - gasPrice: web3.fromWei(gasPrice.toString(), 'gwei') - }) - } - }); - } -} +// export const getGasPrice = (): Promise => { +// return (dispatch, getState) => { +// web3.eth.getGasPrice((error, gasPrice) => { +// if (!error) { +// dispatch({ +// type: 'update_gas', +// gasPrice: web3.fromWei(gasPrice.toString(), 'gwei') +// }) +// } +// }); +// } +// } const Web3Service = store => next => action => { - switch (action.type) { - - // case ACTIONS.ON_TX_COMPLETE : - // pendingTxs.push(action.txid); - - // // const refreshBalance = async (sender) => { - // // let balance = await getBalance(sender.address); - // // store.dispatch({ - // // type: ACTIONS.ADDRESS_SET_BALANCE, - // // address: sender, - // // balance: web3.fromWei(balance.toString(), 'ether') - // // }) - // // } - - // // const sender = action.address; - // // setInterval( async () =>{ - // // let balance = await getBalance(sender.address); - // // console.log("update balance", web3.fromWei(balance.toString(), 'ether') ) - // // }, 2000); - - // break; + next(action); - // case ACTIONS.TX_STATUS_OK : - // let pendingIndex = pendingTxs.indexOf(action.txid); - // if (pendingIndex >= 0) { - // pendingTxs.splice(pendingIndex, 1); - // } - // break; + switch (action.type) { - case LOCATION_CHANGE : + case 'WEB_2_START' : if (web3) break; - web3 = new Web3(window.web3.currentProvider); + //web3 = new Web3(window.web3.currentProvider); //web3 = new Web3(new Web3.providers.HttpProvider("https://api.myetherapi.com/rop")); - //web3 = new Web3(new Web3.providers.HttpProvider("https://ropsten.infura.io/QGyVKozSUEh2YhL4s2G4")); - //web3 = new Web3("ws://34.230.234.51:30303"); - //web3 = new Web3("ws://58.56.184.146:45536"); - //web3 = new Web3(new Web3.providers.HttpProvider('https://api.myetherapi.com/rop')); - //web3.setProvider(new Web3.providers.HttpProvider('https://api.myetherapi.com/rop') ); + web3 = new Web3(new Web3.providers.HttpProvider("https://ropsten.infura.io2/QGyVKozSUEh2YhL4s2G4")); //web3 = new Web3( new Web3.providers.HttpProvider("ws://34.230.234.51:30303") ); + + + + /*store.dispatch( getGasPrice() ); + + + const latestBlockFilter = web3.eth.filter('latest'); + latestBlockFilter.watch((error, blockHash) => { + + const { addresses, pendingTxs } = store.getState().addresses; + + for (const addr of addresses) { + store.dispatch( getBalance(addr) ); + } + + store.dispatch( getGasPrice() ); + + if (pendingTxs.length > 0) { + for (const tx of pendingTxs) { + store.dispatch( getTransactionReceipt(tx) ); + } + } + });*/ + + + + // store.dispatch({ + // type: 'web3__init', + // web3 + // }); + // store.dispatch({ // type: 'update_gas', // gasPrice: web3.fromWei(web3.eth.gasPrice, 'gwei') // }) - const latestBlockFilter = web3.eth.filter('latest'); - //const latestBlockFilter = web3.eth.filter('pending'); - latestBlockFilter.watch((error, txid) => { - //console.log("Watch latest block", txid, error); + /* + { + "dd62ed3e": "allowance(address,address)", + "095ea7b3": "approve(address,uint256)", + "cae9ca51": "approveAndCall(address,uint256,bytes)", + "70a08231": "balanceOf(address)", + "313ce567": "decimals()", + "06fdde03": "name()", + "95d89b41": "symbol()", + "18160ddd": "totalSupply()", + "a9059cbb": "transfer(address,uint256)", + "23b872dd": "transferFrom(address,address,uint256)", + "54fd4d50": "version()" + }*/ + + // var balanceHex = "06fdde03"; // I believe this is the hex for balance + // var contractAddress = "0x58cda554935e4a1f2acbe15f8757400af275e084"; + // var userAddress = "0x5DBB9793537515398A1176d365b636A5321D9e39"; + // var balanceCall = getDataObj(contractAddress, balanceHex); + // var balance = web3.eth.call(balanceCall); + + + + + const abiArray = [{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"version","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"},{"name":"_extraData","type":"bytes"}],"name":"approveAndCall","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"remaining","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"payable":false,"stateMutability":"nonpayable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_from","type":"address"},{"indexed":true,"name":"_to","type":"address"},{"indexed":false,"name":"_value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_owner","type":"address"},{"indexed":true,"name":"_spender","type":"address"},{"indexed":false,"name":"_value","type":"uint256"}],"name":"Approval","type":"event"}]; + //const contr = web3.eth.contract(abiArray, '0x58cda554935e4a1f2acbe15f8757400af275e084'); + const contr = web3.eth.contract(abiArray).at('0x58cda554935e4a1f2acbe15f8757400af275e084'); + console.log("contr", contr ); + + contr.name.call((e,r) => { + console.log("nameeeee", e, r) + }) - store.dispatch( getGasPrice() ); + contr.symbol.call((e,r) => { + console.log("symboll", e, r) + }) - const { pendingTxs } = store.getState().addresses; + //console.log( const.name ) - if (pendingTxs.length > 0) { - // let pendingTxIndex = pendingTxs.indexOf(txid); - // console.error("---->>>Watch latest block", pendingTxIndex, txid, pendingTxs) - // if (pendingTxIndex >= 0) { - // pendingTxs.splice(pendingTxIndex, 1); - // } + // contr.balanceOf('0x98ead4bd2fbbb0cf0b49459aa0510ef53faa6cad', (e, r) => { + // console.warn('contrR', e, r.toString(10)); + // }); - store.dispatch( loadTransactionStatus(pendingTxs[0]) ); - } + let cntrData = contr.transfer.getData("0x98ead4bd2fbbb0cf0b49459aa0510ef53faa6cad", 1, { + from: "0x98ead4bd2fbbb0cf0b49459aa0510ef53faa6cad", + gasLimit: 36158, + gasPrice: "0x0ee6b28000" + }) - // if (!error) { - // web3.eth.getTransactionReceipt(txid, (error, tx) => { - // console.log("LatestTX", txid, tx, error) - // }) - // } - }); + console.log("contr", cntrData); + // const data = contr.transferFrom( + // '0x98ead4bd2fbbb0cf0b49459aa0510ef53faa6cad', + // '0x98ead4bd2fbbb0cf0b49459aa0510ef53faa6cad', + // 1 + // ); + + // const data = contr.transferFrom( + // '0x98ead4bd2fbbb0cf0b49459aa0510ef53faa6cad', + // { + // from: '0x7314e0f1c0e28474bdb6be3e2c3e0453255188f8', + // value: 1 + // } + // ); + + // const data = contr.transferFrom( + // '0x00000000000000000000000098ead4bd2fbbb0cf0b49459aa0510ef53faa6cad', + // '0x000000000000000000000000a738ea40b69d87f4f9ac94c9a0763f96248df23b', + // 2 + // ); + //console.log("contr", contr, data) + + // var addr1 = '0x98ead4bd2fbbb0cf0b49459aa0510ef53faa6cad'; + // var contractAddr = '0x58cda554935e4a1f2acbe15f8757400af275e084'; + // var tknAddress = (addr1).substring(2); + // var contractData = ('0x70a08231000000000000000000000000' + tknAddress); + + // console.warn("ADDDDDDDD", web3.toHex('0x98ead4bd2fbbb0cf0b49459aa0510ef53faa6cad')); + // console.warn("ADDDDDDDD", web3.toHex('98ead4bd2fbbb0cf0b49459aa0510ef53faa6cad')); + + // web3.eth.call({ + // to: contractAddr, + // data: contractData + // }, function(err, result) { + // if (result) { + // console.log("---------result", result, web3); + // //var tokens = web3.toBN(result).toString(); + // //console.log('Tokens Owned: ' + web3.utils.fromWei(tokens, 'ether')); + // } + // else { + // console.log(err); // Dump errors here + // } + // }); + + /* const pendingBlockFilter = web3.eth.filter('pending'); @@ -145,49 +214,16 @@ const Web3Service = store => next => action => { //"web3": "^0.19.0" - console.log("WEB#", web3) - store.dispatch({ - type: 'web3__init', - web3 - }) + break; } - next(action); + }; export default Web3Service; -const updateGas = async () => { - -} - -export const watchPendingTx = (address: string): Promise => { - return new Promise((resolve, reject) => { - web3.eth.getTransaction(txid, (error, result) => { - if (error) { - reject(error); - } else { - resolve(result); - } - }); - }); -} - -export const getBalance = (address: string): Promise => { - return new Promise((resolve, reject) => { - web3.eth.getBalance(address, (error, result) => { - if (error) { - reject(error); - } else { - resolve(result); - } - }); - }); -} - - export const estimateGas = (gasOptions): Promise => { return new Promise((resolve, reject) => { diff --git a/src/js/services/index.js b/src/js/services/index.js index 318e6a30..4e68a0b4 100644 --- a/src/js/services/index.js +++ b/src/js/services/index.js @@ -2,13 +2,17 @@ 'use strict'; import RouterService from './RouterService'; +import LocalStorageService from './LocalStorageService'; +import CoinmarketcapService from './CoinmarketcapService'; import TrezorConnectService from './TrezorConnectService'; import Web3Service from './Web3Service'; import EtherscanService from './EtherscanService'; export default [ RouterService, + LocalStorageService, TrezorConnectService, Web3Service, - EtherscanService, + CoinmarketcapService, + //EtherscanService, ]; \ No newline at end of file diff --git a/src/js/store/store.dev.js b/src/js/store/store.dev.js index ee97c457..4120d399 100644 --- a/src/js/store/store.dev.js +++ b/src/js/store/store.dev.js @@ -2,9 +2,10 @@ 'use strict'; import { createStore, applyMiddleware, compose } from 'redux'; -import { routerMiddleware, push } from 'react-router-redux'; +import { syncHistoryWithStore, routerMiddleware, push } from 'react-router-redux'; import thunk from 'redux-thunk'; -//import createHistory from 'history/createBrowserHistory'; +// import createHistory from 'history/createBrowserHistory'; +// import { useRouterHistory } from 'react-router'; import createHistory from 'history/createHashHistory'; import { createLogger } from 'redux-logger'; import reducers from '../reducers'; @@ -12,7 +13,7 @@ import services from '../services'; import { Middleware } from 'redux'; import { GenericStoreEnhancer } from 'redux'; -export const history = createHistory(); +export const history = createHistory( { queryKey: false } ); const initialState: any = {}; const enhancers = []; @@ -23,7 +24,7 @@ const middleware = [ const excludeLogger = (getState: any, action: any): boolean => { //'@@router/LOCATION_CHANGE' - let excluded = ['MQTT_PING']; + let excluded = ['LOG_TO_EXCLUDE']; let pass = excluded.filter((act) => { return action.type === act; }); diff --git a/src/js/utils/networkUtils.js b/src/js/utils/networkUtils.js index e135d70d..c1b44893 100644 --- a/src/js/utils/networkUtils.js +++ b/src/js/utils/networkUtils.js @@ -15,7 +15,7 @@ export const httpRequest = async (url: string, type: string = 'text'): any => { return await response.text(); } } else { - throw new Error(response.statusText); + throw new Error(`${ url } ${ response.statusText }`); } // return fetch(url, { credentials: 'same-origin' }).then((response) => { diff --git a/src/js/utils/promiseUtils.js b/src/js/utils/promiseUtils.js new file mode 100644 index 00000000..f1785b45 --- /dev/null +++ b/src/js/utils/promiseUtils.js @@ -0,0 +1,12 @@ +/* @flow */ +'use strict'; + +// import root from 'window-or-global'; +// import Promise from 'es6-promise'; + +export async function resolveAfter(msec: number, value: any = null): Promise { + return await new Promise((resolve) => { + //root.setTimeout(resolve, msec, value); + window.setTimeout(resolve, msec, value); + }); +} \ No newline at end of file diff --git a/src/js/utils/reducerUtils.js b/src/js/utils/reducerUtils.js new file mode 100644 index 00000000..a8e2a83e --- /dev/null +++ b/src/js/utils/reducerUtils.js @@ -0,0 +1,12 @@ +/* @flow */ +'use strict'; + + +export const getAccounts = (accounts: Array, device: any, coin: ?string): Array => { + if (coin) { + return accounts.filter((addr) => addr.checksum === device.checksum && addr.coin === coin); + } else { + return accounts.filter((addr) => addr.checksum === device.checksum); + } + +} \ No newline at end of file diff --git a/src/js/utils/windowUtils.js b/src/js/utils/windowUtils.js new file mode 100644 index 00000000..4f2894c4 --- /dev/null +++ b/src/js/utils/windowUtils.js @@ -0,0 +1,18 @@ +/* @flow */ +'use strict'; + +export const getViewportHeight = (): number => ( + window.innerHeight + || document.documentElement.clientHeight + || document.body.clientHeight +) + +export const getScrollY = (): number => { + if (window.pageYOffset !== undefined) { + return window.pageYOffset; + } else if (window.scrollTop !== undefined) { + return window.scrollTop; + } else { + return (document.documentElement || document.body.parentNode || document.body).scrollTop; + } +} \ No newline at end of file diff --git a/src/solidity/erc20.json b/src/solidity/erc20.json new file mode 100644 index 00000000..7d0efcda --- /dev/null +++ b/src/solidity/erc20.json @@ -0,0 +1,272 @@ +module.exports = [ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_spender", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "name": "success", + "type": "bool" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_from", + "type": "address" + }, + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "name": "success", + "type": "bool" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "name": "", + "type": "uint8" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "version", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "name": "balance", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "name": "success", + "type": "bool" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_spender", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + }, + { + "name": "_extraData", + "type": "bytes" + } + ], + "name": "approveAndCall", + "outputs": [ + { + "name": "success", + "type": "bool" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + }, + { + "name": "_spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "name": "remaining", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "inputs": [ + { + "name": "_initialAmount", + "type": "uint256" + }, + { + "name": "_tokenName", + "type": "string" + }, + { + "name": "_decimalUnits", + "type": "uint8" + }, + { + "name": "_tokenSymbol", + "type": "string" + } + ], + "type": "constructor" + }, + { + "payable": false, + "type": "fallback" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "_from", + "type": "address" + }, + { + "indexed": true, + "name": "_to", + "type": "address" + }, + { + "indexed": false, + "name": "_value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "_owner", + "type": "address" + }, + { + "indexed": true, + "name": "_spender", + "type": "address" + }, + { + "indexed": false, + "name": "_value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + ] \ No newline at end of file diff --git a/src/solidity/lahodka-token.js b/src/solidity/lahodka-token.js new file mode 100644 index 00000000..2c2467b9 --- /dev/null +++ b/src/solidity/lahodka-token.js @@ -0,0 +1,137 @@ +pragma solidity ^0.4.4; + +contract Token { + + /// @return total amount of tokens + function totalSupply() constant returns (uint256 supply) {} + + /// @param _owner The address from which the balance will be retrieved + /// @return The balance + function balanceOf(address _owner) constant returns (uint256 balance) {} + + /// @notice send `_value` token to `_to` from `msg.sender` + /// @param _to The address of the recipient + /// @param _value The amount of token to be transferred + /// @return Whether the transfer was successful or not + function transfer(address _to, uint256 _value) returns (bool success) {} + + /// @notice send `_value` token to `_to` from `_from` on the condition it is approved by `_from` + /// @param _from The address of the sender + /// @param _to The address of the recipient + /// @param _value The amount of token to be transferred + /// @return Whether the transfer was successful or not + function transferFrom(address _from, address _to, uint256 _value) returns (bool success) {} + + /// @notice `msg.sender` approves `_addr` to spend `_value` tokens + /// @param _spender The address of the account able to transfer the tokens + /// @param _value The amount of wei to be approved for transfer + /// @return Whether the approval was successful or not + function approve(address _spender, uint256 _value) returns (bool success) {} + + /// @param _owner The address of the account owning tokens + /// @param _spender The address of the account able to transfer the tokens + /// @return Amount of remaining tokens allowed to spent + function allowance(address _owner, address _spender) constant returns (uint256 remaining) {} + + event Transfer(address indexed _from, address indexed _to, uint256 _value); + event Approval(address indexed _owner, address indexed _spender, uint256 _value); + +} + + + +contract StandardToken is Token { + + function transfer(address _to, uint256 _value) returns (bool success) { + //Default assumes totalSupply can't be over max (2^256 - 1). + //If your token leaves out totalSupply and can issue more tokens as time goes on, you need to check if it doesn't wrap. + //Replace the if with this one instead. + //if (balances[msg.sender] >= _value && balances[_to] + _value > balances[_to]) { + if (balances[msg.sender] >= _value && _value > 0) { + balances[msg.sender] -= _value; + balances[_to] += _value; + Transfer(msg.sender, _to, _value); + return true; + } else { return false; } + } + + function transferFrom(address _from, address _to, uint256 _value) returns (bool success) { + //same as above. Replace this line with the following if you want to protect against wrapping uints. + //if (balances[_from] >= _value && allowed[_from][msg.sender] >= _value && balances[_to] + _value > balances[_to]) { + if (balances[_from] >= _value && allowed[_from][msg.sender] >= _value && _value > 0) { + balances[_to] += _value; + balances[_from] -= _value; + allowed[_from][msg.sender] -= _value; + Transfer(_from, _to, _value); + return true; + } else { return false; } + } + + function balanceOf(address _owner) constant returns (uint256 balance) { + return balances[_owner]; + } + + function approve(address _spender, uint256 _value) returns (bool success) { + allowed[msg.sender][_spender] = _value; + Approval(msg.sender, _spender, _value); + return true; + } + + function allowance(address _owner, address _spender) constant returns (uint256 remaining) { + return allowed[_owner][_spender]; + } + + mapping (address => uint256) balances; + mapping (address => mapping (address => uint256)) allowed; + uint256 public totalSupply; +} + + +//name this contract whatever you'd like +contract Lahodkoin is StandardToken { + + function () { + //if ether is sent to this address, send it back. + throw; + } + + /* Public variables of the token */ + + /* + NOTE: + The following variables are OPTIONAL vanities. One does not have to include them. + They allow one to customise the token contract & in no way influences the core functionality. + Some wallets/interfaces might not even bother to look at this information. + */ + string public name; //fancy name: eg Simon Bucks + uint8 public decimals; //How many decimals to show. ie. There could 1000 base units with 3 decimals. Meaning 0.980 SBX = 980 base units. It's like comparing 1 wei to 1 ether. + string public symbol; //An identifier: eg SBX + string public version = 'H1.0'; //human 0.1 standard. Just an arbitrary versioning scheme. + +// +// CHANGE THESE VALUES FOR YOUR TOKEN +// + +//make sure this function name matches the contract name above. So if you're token is called TutorialToken, make sure the //contract name above is also TutorialToken instead of ERC20Token + + function Lahodkoin( + ) { + balances[msg.sender] = 5; // Give the creator all initial tokens (100000 for example) + totalSupply = 5; // Update total supply (100000 for example) + name = "Lahodkoin"; // Set the name for display purposes + decimals = 0; // Amount of decimals for display purposes + symbol = "LAHODKY"; // Set the symbol for display purposes + } + + /* Approves and then calls the receiving contract */ + function approveAndCall(address _spender, uint256 _value, bytes _extraData) returns (bool success) { + allowed[msg.sender][_spender] = _value; + Approval(msg.sender, _spender, _value); + + //call the receiveApproval function on the contract you want to be notified. This crafts the function signature manually so one doesn't have to include a contract in here just for this. + //receiveApproval(address _from, uint256 _value, address _tokenContract, bytes _extraData) + //it is assumed that when does this that the call *should* succeed, otherwise one would use vanilla approve instead. + if(!_spender.call(bytes4(bytes32(sha3("receiveApproval(address,uint256,address,bytes)"))), msg.sender, _value, this, _extraData)) { throw; } + return true; + } +} \ No newline at end of file diff --git a/src/solidity/test-token.js b/src/solidity/test-token.js new file mode 100644 index 00000000..b83db55c --- /dev/null +++ b/src/solidity/test-token.js @@ -0,0 +1,137 @@ +pragma solidity ^0.4.4; + +contract Token { + + /// @return total amount of tokens + function totalSupply() constant returns (uint256 supply) {} + + /// @param _owner The address from which the balance will be retrieved + /// @return The balance + function balanceOf(address _owner) constant returns (uint256 balance) {} + + /// @notice send `_value` token to `_to` from `msg.sender` + /// @param _to The address of the recipient + /// @param _value The amount of token to be transferred + /// @return Whether the transfer was successful or not + function transfer(address _to, uint256 _value) returns (bool success) {} + + /// @notice send `_value` token to `_to` from `_from` on the condition it is approved by `_from` + /// @param _from The address of the sender + /// @param _to The address of the recipient + /// @param _value The amount of token to be transferred + /// @return Whether the transfer was successful or not + function transferFrom(address _from, address _to, uint256 _value) returns (bool success) {} + + /// @notice `msg.sender` approves `_addr` to spend `_value` tokens + /// @param _spender The address of the account able to transfer the tokens + /// @param _value The amount of wei to be approved for transfer + /// @return Whether the approval was successful or not + function approve(address _spender, uint256 _value) returns (bool success) {} + + /// @param _owner The address of the account owning tokens + /// @param _spender The address of the account able to transfer the tokens + /// @return Amount of remaining tokens allowed to spent + function allowance(address _owner, address _spender) constant returns (uint256 remaining) {} + + event Transfer(address indexed _from, address indexed _to, uint256 _value); + event Approval(address indexed _owner, address indexed _spender, uint256 _value); + +} + + + +contract StandardToken is Token { + + function transfer(address _to, uint256 _value) returns (bool success) { + //Default assumes totalSupply can't be over max (2^256 - 1). + //If your token leaves out totalSupply and can issue more tokens as time goes on, you need to check if it doesn't wrap. + //Replace the if with this one instead. + //if (balances[msg.sender] >= _value && balances[_to] + _value > balances[_to]) { + if (balances[msg.sender] >= _value && _value > 0) { + balances[msg.sender] -= _value; + balances[_to] += _value; + Transfer(msg.sender, _to, _value); + return true; + } else { return false; } + } + + function transferFrom(address _from, address _to, uint256 _value) returns (bool success) { + //same as above. Replace this line with the following if you want to protect against wrapping uints. + //if (balances[_from] >= _value && allowed[_from][msg.sender] >= _value && balances[_to] + _value > balances[_to]) { + if (balances[_from] >= _value && allowed[_from][msg.sender] >= _value && _value > 0) { + balances[_to] += _value; + balances[_from] -= _value; + allowed[_from][msg.sender] -= _value; + Transfer(_from, _to, _value); + return true; + } else { return false; } + } + + function balanceOf(address _owner) constant returns (uint256 balance) { + return balances[_owner]; + } + + function approve(address _spender, uint256 _value) returns (bool success) { + allowed[msg.sender][_spender] = _value; + Approval(msg.sender, _spender, _value); + return true; + } + + function allowance(address _owner, address _spender) constant returns (uint256 remaining) { + return allowed[_owner][_spender]; + } + + mapping (address => uint256) balances; + mapping (address => mapping (address => uint256)) allowed; + uint256 public totalSupply; +} + + +//name this contract whatever you'd like +contract ERC20Token is StandardToken { + + function () { + //if ether is sent to this address, send it back. + throw; + } + + /* Public variables of the token */ + + /* + NOTE: + The following variables are OPTIONAL vanities. One does not have to include them. + They allow one to customise the token contract & in no way influences the core functionality. + Some wallets/interfaces might not even bother to look at this information. + */ + string public name; //fancy name: eg Simon Bucks + uint8 public decimals; //How many decimals to show. ie. There could 1000 base units with 3 decimals. Meaning 0.980 SBX = 980 base units. It's like comparing 1 wei to 1 ether. + string public symbol; //An identifier: eg SBX + string public version = 'H1.0'; //human 0.1 standard. Just an arbitrary versioning scheme. + +// +// CHANGE THESE VALUES FOR YOUR TOKEN +// + +//make sure this function name matches the contract name above. So if you're token is called TutorialToken, make sure the //contract name above is also TutorialToken instead of ERC20Token + + function ERC20Token( + ) { + balances[msg.sender] = 1000; // Give the creator all initial tokens (100000 for example) + totalSupply = 1000; // Update total supply (100000 for example) + name = "Trezor01"; // Set the name for display purposes + decimals = 0; // Amount of decimals for display purposes + symbol = "T01"; // Set the symbol for display purposes + } + + /* Approves and then calls the receiving contract */ + function approveAndCall(address _spender, uint256 _value, bytes _extraData) returns (bool success) { + allowed[msg.sender][_spender] = _value; + Approval(msg.sender, _spender, _value); + + //call the receiveApproval function on the contract you want to be notified. This crafts the function signature manually so one doesn't have to include a contract in here just for this. + //receiveApproval(address _from, uint256 _value, address _tokenContract, bytes _extraData) + //it is assumed that when does this that the call *should* succeed, otherwise one would use vanilla approve instead. + if(!_spender.call(bytes4(bytes32(sha3("receiveApproval(address,uint256,address,bytes)"))), msg.sender, _value, this, _extraData)) { throw; } + return true; + } +} \ No newline at end of file diff --git a/src/styles/accounts.less b/src/styles/accounts.less deleted file mode 100644 index f8d542af..00000000 --- a/src/styles/accounts.less +++ /dev/null @@ -1,27 +0,0 @@ -.accounts { - width: 25%; - - .header { - background: red; - } - - a { - position: relative; - display: block; - cursor: pointer; - padding: 10px 15px; - white-space: nowrap; - color: black; - &.selected { - background: #FFFFFF; - border-left: 4px solid @color_green; - padding-left: 11px; - } - - span { - display: block; - font-size: 12px; - color: gray; - } - } -} \ No newline at end of file diff --git a/src/styles/acquire.less b/src/styles/acquire.less new file mode 100644 index 00000000..93eb6b3f --- /dev/null +++ b/src/styles/acquire.less @@ -0,0 +1,50 @@ +.acquire { + flex: 1; + display: flex; + flex-direction: column; + background: @color_white; + + .warning { + background: @color_info_secondary; + display: flex; + flex-direction: row; + padding: 26px 39px 26px 80px; + + div { + flex: 1; + position: relative; + } + + h2 { + color: @color_info_primary; + font-size: 14px; + -webkit-font-smoothing: auto; + margin-bottom: 5px; + padding: 0px; + + &:before { + .icomoon-info; + position: absolute; + top: -7px; + left: -32px; + font-size: 32px; + } + } + + p { + color: @color_info_primary; + font-size: 12px; + padding: 0px; + } + + } + + // h2 { + // line-height: 74px; + // padding-left: 50px; + // } + + // p { + // padding-left: 50px; + // } +} \ No newline at end of file diff --git a/src/styles/aside.less b/src/styles/aside.less new file mode 100644 index 00000000..f2e5954b --- /dev/null +++ b/src/styles/aside.less @@ -0,0 +1,471 @@ +aside { + position: relative; + width: 320px; + min-width: 320px; + border-right: 1px solid @color_divider; + //display: flex; + //flex-direction: column; + overflow-x: hidden; + + .Select { + width: 320px; + height: 64px; + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.04); + + .Select-control { + height: 63px; + border: 0px; + // border-radius: 4px 0px 0px 0px; + border-right: 1px solid @color_divider; + border-bottom: 1px solid @color_divider; + transition: color 0.3s ease-in-out; + } + + .Select-arrow-zone { + right: 24px; + } + + .Select-menu-outer { + visibility: hidden; + } + + &.is-open { + .Select-control { + border-color: @color_divider; + } + } + + &.is-disabled { + .Select-control { + background: @color_white; + cursor: default; + } + + .Select-arrow { + visibility: hidden; + &:after { + content: '' + } + } + + .device { + .device-menu { + padding-right: 24px; + } + } + } + } + + .sticky-container { + position: relative; + top: 0; + width: 320px; + overflow: hidden; + + &.fixed { + position: fixed; + border-right: 1px solid @color_divider; + } + + &.fixed-bottom { + padding-bottom: 60px; // height of .help + .help { + position: fixed; + bottom: 0; + background: @color_main; + border-right: 1px solid @color_divider; + } + } + } + + .transition-container { + width: 640px; + + section { + width: 320px; + display: inline-block; + vertical-align: top; + } + } + + .device { + position: relative; + height: 63px; + width: 319px; + display: flex; + align-items: center; + padding-left: 80px; + + &.item { + padding-right: 24px; + cursor: pointer; + .hover(); + &:hover { + background: @color_gray_light; + } + } + + &:before { + content: ''; + position: absolute; + display: block; + width: 13px; + height: 25px; + z-index: 2; + left: 33px; + top: 17px; + background-repeat: no-repeat; + background-position: center; + background-size: 13px 25px; + background-image: url('../images/icontrezor.png'); + } + + .label-container { + flex: 1; + overflow: hidden; + span { + display: block; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + + &.label { + font-weight: 500; + font-size: 14px; + color: @color_text_primary; + } + + &.status { + font-size: 12px; + color: @color_text_secondary; + } + } + } + + .device-menu { + display: flex; + justify-content: flex-end; + padding-right: 48px; + padding-left: 4px; + + div { + display: inline-block; + } + + .forget, + .settings, + .acquire { + cursor: pointer; + + &:before { + .icomoon-refresh; + color: @color_text_secondary; + position: relative; + font-size: 24px; + .hover(); + } + + &:hover { + &:before { + color: @color_text_primary; + } + } + } + + .forget { + &:before { + .icomoon-eject; + } + } + + .settings { + &:before { + .icomoon-settings; + } + } + } + + } + + a { + position: relative; + display: block; + cursor: pointer; + font-size: 16px; + padding: 16px 0 16px 30px; + white-space: nowrap; + color: @color_text_primary; + height: 50px; + + .hover(); + + &:hover { + background: @color_gray_light; + } + + &.account { + height: 64px; + display: flex; + flex-direction: column; + justify-content: space-evenly; + font-size: 14px; + border-top: 1px solid @color_divider; + span { + display: block; + font-size: 12px; + color: @color_text_secondary; + } + + // &:last-child { + // border-bottom: 1px solid @color_divider; + // } + } + + &.selected { + background: @color_white; + border-left: 3px solid @color_green_primary; + padding-left: 27px; + + &:hover { + background: @color_white; + } + } + + &.coin { + padding-left: 80px; + &:before { + content: ''; + position: absolute; + display: block; + width: 20px; + height: 20px; + left: 30px; + top: 0px; + bottom: 0px; + margin: auto 0; + background-repeat: no-repeat; + background-position: center; + background-size: 20px 20px; + + } + } + + &.external { + &:after { + .icomoon-redirect; + position: absolute; + display: block; + width: 30px; + height: 30px; + right: 23px; + top: 0px; + bottom: 0px; + margin: auto 0; + font-size: 30px; + color: @color_text_secondary; + .hover(); + } + + &:hover:after { + color: @color_text_primary; + } + } + + &.back { + padding-left: 100px; + + &:before { + content: ''; + position: absolute; + display: block; + width: 20px; + height: 20px; + left: 76px; + top: 0px; + bottom: 0px; + margin: auto 0; + background-repeat: no-repeat; + background-position: center; + background-size: 20px 20px; + } + + &:after { + .icomoon-arrow-left; + position: absolute; + display: block; + width: 20px; + height: 20px; + left: 24px; + top: 0px; + bottom: 0px; + margin: auto 0; + font-size: 20px; + } + } + + &.eth:before { + background-image: url('../images/eth-logo.png'); + background-size: auto 20px; + } + &.etc:before { + background-image: url('../images/etc-logo.png'); + background-size: auto 20px; + } + + &.btc:before { + background-image: url('../images/btc-logo.png'); + } + &.bch:before { + background-image: url('../images/bch-logo.png'); + } + &.btg:before { + background-image: url('../images/btg-logo.png'); + } + &.ltc:before { + background-image: url('../images/ltc-logo.png'); + } + &.dash:before { + background-image: url('../images/dash-logo.png'); + } + &.zec:before { + background-image: url('../images/zec-logo.png'); + } + + + } + + .coin-divider { + font-size: 12px; + display: flex; + justify-content: space-between; + color: @color_text_secondary; + background: @color_gray_light; + padding: 8px 30px 8px 31px; + border-top: 1px solid @color_divider; + border-bottom: 1px solid @color_divider; + span { + display: flex; + justify-content: flex-end; + } + } + + .help { + width: 320px; + padding: 14px 0px; + text-align: center; + border-top: 1px solid @color_divider; + + &.fixed { + position: fixed; + bottom: 0px; + } + + a { + color: @color_text_secondary; + font-size: 12px; + display: inline-block; + padding: 8px; + height: auto; + + &:before { + .icomoon-chat; + font-size: 32px; + position: absolute; + top: 0px; + left: -26px; + } + + &:hover { + background: transparent; + color: @color_text_primary; + } + } + + } + + + + .add-address { + position: relative; + padding: 4px 0 4px 20px; + cursor: pointer; + color: @color_text_secondary; + display: flex; + align-items: center; + + &:before { + .icomoon-plus; + margin-right: 12px; + } + + .hover(); + &:hover { + color: @color_text_primary; + } + } + + .discovery-status { + height: 64px; + display: flex; + flex-direction: column; + justify-content: space-evenly; + font-size: 14px; + padding: 16px 0 16px 30px; + white-space: nowrap; + border-top: 1px solid @color_divider; + span { + display: block; + font-size: 12px; + color: @color_text_secondary; + } + } + + .discovery-loading { + display: flex; + flex-direction: row; + align-items: center; + font-size: 14px; + padding: 16px 0 16px 30px; + white-space: nowrap; + border-top: 1px solid @color_divider; + .loader-circle { + margin-right: 12px; + } + } + + // menu trasitions + + @slide_transition_time: 300ms; + + .slide-left-enter { + transform: translate(100%); + pointer-events: none; + } + .slide-left-enter.slide-left-enter-active { + transform: translate(0%); + transition: transform @slide_transition_time ease-in-out; + } + .slide-left-exit { + transform: translate(-100%); + } + .slide-left-exit.slide-left-exit-active { + transform: translate(0%); + transition: transform @slide_transition_time ease-in-out; + } + + .slide-right-enter { + transform: translate(-100%); + pointer-events: none; + } + .slide-right-enter.slide-right-enter-active { + transform: translate(0%); + transition: transform @slide_transition_time ease-in-out; + } + .slide-right-exit { + transform: translate(-100%); + } + .slide-right-exit.slide-right-exit-active { + transform: translate(-200%); + transition: transform @slide_transition_time ease-in-out; + } +} \ No newline at end of file diff --git a/src/styles/base.less b/src/styles/base.less index f431acca..1a3aa928 100644 --- a/src/styles/base.less +++ b/src/styles/base.less @@ -10,49 +10,64 @@ *:focus, *:active, *:active:focus, *::selection, *::-moz-selection { outline: 0 !important; -webkit-appearance: none; - -webkit-tap-highlight-color: rgba(0,0,0,0); + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } html, body { width: 100%; height: 100%; -} - -html, body { position: relative; - background-color: @color_white; + background-color: @color_body; font-family: @font-default; + font-weight: 300; font-size: 14px; - -webkit-font-smoothing: antialiased; } -.layout-wrapper { - width: 1170px; - margin: 0 auto; - padding: 0 15px; +.app { + position: relative; + min-height: 100vh; + min-width: 720px; + display: flex; + flex-direction: column; + &.resized { + // to make sure that unpacked coin menu will not overflow main container + // 512 dropdown height + 50 header + 30 margin + 64 topnav height + min-height: 680px; + } } main { - width: 1170px; - min-height: 100%; - padding-top: 90px; - padding-bottom: 25px; + width: 100%; + max-width: 1170px; margin: 0 auto; - //flex: 1; - background: @color_main_background; + flex: 1; + background: @color_main; display: flex; flex-direction: row; + border-radius: 4px 4px 0px 0px; + overflow: hidden; + margin-top: 32px; + + @media screen and (max-width: 1170px) { + border-radius: 0px; + margin-top: 0px; + } } + a { text-decoration: none; cursor: pointer; } -a:focus { - .no-outlines(); +a:focus, +button:focus, +input:focus, +textarea:focus { + outline: 0; } -button:focus, input:focus, textarea:focus { - outline: 0; +h1, h2, h3 { + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; } \ No newline at end of file diff --git a/src/styles/colors.less b/src/styles/colors.less index a771fb45..45f974c9 100644 --- a/src/styles/colors.less +++ b/src/styles/colors.less @@ -1,12 +1,30 @@ @color_white: #ffffff; -@color_white_smoke: #F5F5F5; -@color_light_gray: #D3D3D3; -@color_main_background: #f6f7f8; +@color_header: #212121; +@color_body: #EBEBEB; +@color_main: #FBFBFB; +@color_landing: #F9F9F9; -@color_primary_text: #333333; -@color_secondary_text: #666666; -@color_link: #000000; -@color_link_visited: #777777; +/// new!!! -@color_green: #4cc148; +@color_text_primary: #505050; +@color_text_secondary: #A9A9A9; + +@color_gray_light: #F2F2F2; // hover menu +@color_divider: #EBEBEB; + +@color_green_primary: #01B757; +@color_green_secondary: #00AB51; +@color_green_tertiary: #009546; + +@color_info_primary: #1E7FF0; +@color_info_secondary: #E1EFFF; + +@color_warning_primary: #EB8A00; +@color_warning_secondary: #FFEFD9; + +@color_success_primary: #01B757; +@color_success_secondary: #DFFFEE; + +@color_error_primary: #ED1212; +@color_error_secondary: #FFE9E9; \ No newline at end of file diff --git a/src/styles/content.less b/src/styles/content.less new file mode 100644 index 00000000..6390f93e --- /dev/null +++ b/src/styles/content.less @@ -0,0 +1,60 @@ +article { + flex: 1; + display: flex; + flex-direction: column; + + nav { + height: 64px; + border-bottom: 1px solid @color_divider; + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.06); + display: flex; + background: @color_white; + position: relative; + + .account-tabs { + display: flex; + flex: 1; + align-items: center; + justify-content: space-between; + padding: 0px 48px; + max-width: 600px; + + a { + font-weight: 500; + font-size: 14px; + color: @color_text_secondary; + margin: 0px 4px; + &.active, + &:hover { + color: @color_text_primary; + } + + &:first-child { + margin-left: 0px; + } + + &:last-child { + margin-right: 0px; + } + } + } + } + + section { + flex: 1; + display: flex; + flex-direction: column; + background: @color_white; + + h2 { + font-size: 16px; + font-weight: 500; + padding: 24px 48px; + } + + p { + padding: 0px 48px; + color: @color_text_secondary; + } + } +} \ No newline at end of file diff --git a/src/styles/dashboard.less b/src/styles/dashboard.less new file mode 100644 index 00000000..a9dc9b84 --- /dev/null +++ b/src/styles/dashboard.less @@ -0,0 +1,22 @@ +.dashboard { + + //height: 1000px; + + .row { + flex: 1; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding-bottom: 98px; + + h2 { + padding: 0; + } + + p { + padding: 24px 0px; + } + } +} \ No newline at end of file diff --git a/src/styles/devices.less b/src/styles/devices.less deleted file mode 100644 index db506e30..00000000 --- a/src/styles/devices.less +++ /dev/null @@ -1,46 +0,0 @@ -nav { - position: fixed; - top: 50px; - width: 100%; - z-index: 100; - - .layout-wrapper { - color: @color_white; - background: #2C2C2C; - padding: 0; - } - - ul { - list-style: none; - - li { - position: relative; - display: block; - cursor: pointer; - padding: 10px 15px; - white-space: nowrap; - //overflow: hidden; - width: 25%; - display: inline-block; - border-top: 1px solid transparent; - border-bottom: 4px solid transparent; - &.active { - background: #060606; - border-top-color: #2C2C2C; - border-bottom-color: #4cc148; - } - - &.unacquired { - color: gray; - } - - &.used-elsewhere { - color: red !important; - } - - &.reload-features { - color: orange; - } - } - } -} diff --git a/src/styles/fonts.less b/src/styles/fonts.less index 4eeb9176..81328954 100644 --- a/src/styles/fonts.less +++ b/src/styles/fonts.less @@ -1,7 +1,16 @@ +// custom Roboto with Zero without the thing inside, so it's more readable as number +// since 0 doesn't look too similar to 8 +@font-face { + font-family: 'Roboto Zero'; + src: url('../fonts/roboto/RobotoZero.eot') format('embedded-opentype'), + url('../fonts/roboto/RobotoZero.eot?#iefix') format('embedded-opentype'), + url('../fonts/roboto/RobotoZero.woff') format('woff'), + url('../fonts/roboto/RobotoZero.ttf') format('truetype'); +} + @font-face { font-family: 'Roboto Mono'; font-style: normal; - font-weight: 400; src: url('../fonts/roboto/roboto-mono-v4-greek_cyrillic-ext_greek-ext_latin_cyrillic_vietnamese_latin-ext-regular.eot') format('embedded-opentype'), /* IE9 Compat Modes */ url('../fonts/roboto/roboto-mono-v4-greek_cyrillic-ext_greek-ext_latin_cyrillic_vietnamese_latin-ext-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ url('../fonts/roboto/roboto-mono-v4-greek_cyrillic-ext_greek-ext_latin_cyrillic_vietnamese_latin-ext-regular.woff2') format('woff2'), /* Super Modern Browsers */ @@ -10,5 +19,219 @@ url('../fonts/roboto/roboto-mono-v4-greek_cyrillic-ext_greek-ext_latin_cyrillic_vietnamese_latin-ext-regular.svg#RobotoMono') format('svg'); /* Legacy iOS */ } +@font-face { + font-family: 'glyphicons'; + src: url('../fonts/glyphicons.eot') format('embedded-opentype'), + url('../fonts/glyphicons.eot?#iefix') format('embedded-opentype'), + url('../fonts/glyphicons.woff') format('woff'), + url('../fonts/glyphicons.ttf') format('truetype'), + url('../fonts/glyphicons.svg#icomoon') format('svg'); +} + +@font-face { + font-family: 'icomoon'; + src: url('../fonts/icomoon.eot') format('embedded-opentype'), + url('../fonts/icomoon.eot?#iefix') format('embedded-opentype'), + url('../fonts/icomoon.woff') format('woff'), + url('../fonts/icomoon.ttf') format('truetype'), + url('../fonts/icomoon.svg#icomoon') format('svg'); +} + +@font-face { + font-family: 'fontello'; + src: url('../fonts/pass.ttf') format('truetype'); +} + @font-default: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica Neue", Arial, sans-serif; -@font-family-monospace: "Roboto Mono", Menlo, Monaco, Consolas, "Courier New", monospace; \ No newline at end of file +@font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica Neue", Arial, sans-serif; +@font-family-monospace: "Roboto Mono", Menlo, Monaco, Consolas, "Courier New", monospace; +@font-family-monospace-numbers: "Roboto Zero", "Roboto Mono", Menlo, Monaco, Consolas, "Courier New", monospace; + +// ::selection, +// ::-moz-selection { +// background: @color_info_secondary; +// } + +.glyphicon-base() { + display: inline-block; + font-family: 'glyphicons'; + font-style: normal; + font-weight: normal; + line-height: 1; +} + +.glyphicon-trezor { + .glyphicon-base(); + content: "\5f"; + padding-top: 1px; + font-size: 14px; +} + +.glyphicon-info { + .glyphicon-base(); + content: "\ea0c"; +} + +.glyphicon-warning { + .glyphicon-base(); + content: "\ea07"; +} + +.glyphicon-cross { + .glyphicon-base(); + content: "\ea0f"; +} + +.glyphicon-checkmark { + .glyphicon-base(); + content: "\ea10"; +} + +.glyphicon-up { + .glyphicon-base(); + content: "\e113"; +} + +.glyphicon-down { + .glyphicon-base(); + content: "\e114"; +} + +.glyphicon-eye-open { + .glyphicon-base(); + content: "\e105"; +} + +.glyphicon-settings { + .glyphicon-base(); + content: "\e019"; +} + +.glyphicon-refresh { + .glyphicon-base(); + content: "\e031"; +} + +.glyphicon-plus { + .glyphicon-base(); + content: "\2b"; + position: relative; + top: 1px; +} + + +.icomoon-base() { + display: inline-block; + font-family: 'icomoon'; + font-style: normal; + font-weight: normal; + line-height: 1; + font-size: 24px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.004); +} + + + + +.icomoon-eject { + .icomoon-base(); + content: "\e902"; +} + +.icomoon-info { + .icomoon-base(); + content: "\e904"; +} + +.icomoon-refresh { + .icomoon-base(); + content: "\e903"; +} + +.icomoon-chat { + .icomoon-base(); + content: "\e905"; +} + +.icomoon-redirect { + .icomoon-base(); + content: "\e906"; +} + +.icomoon-settings { + .icomoon-base(); + content: "\e907"; +} + +.icomoon-warning { + .icomoon-base(); + content: "\e908"; +} + +.icomoon-arrow-down { + .icomoon-base(); + content: "\e909"; +} + +.icomoon-eye-error { + .icomoon-base(); + content: "\e912"; +} + +.icomoon-T1 { + .icomoon-base(); + content: "\e913"; +} + +.icomoon-close { + .icomoon-base(); + content: "\e90a"; +} + +.icomoon-arrow-left { + .icomoon-base(); + content: "\e90b"; +} + +.icomoon-arrow-up { + .icomoon-base(); + content: "\e90c"; +} + +.icomoon-arrow-right { + .icomoon-base(); + content: "\e90d"; +} + +.icomoon-plus { + .icomoon-base(); + content: "\e90e"; +} + +.icomoon-help { + .icomoon-base(); + content: "\e90f"; +} + + +.icomoon-setmax { + .icomoon-base(); + content: "\e91b"; +} + +.icomoon-checked { + .icomoon-base(); + content: "\e91c"; +} + +.icomoon-error { + .icomoon-base(); + content: "\e91d"; +} + +.icomoon-eye { + .icomoon-base(); + content: "\e91e"; +} diff --git a/src/styles/footer.less b/src/styles/footer.less index b340ac98..2cb033b8 100644 --- a/src/styles/footer.less +++ b/src/styles/footer.less @@ -1,18 +1,22 @@ footer { width: 100%; - position: fixed; - bottom: 0; font-size: 12px; - z-index: 100; - a, a:visited { - color: @color_green; - &:hover { - text-decoration: none; - } + color: @color_text_secondary; + padding: 22px 48px; + border-top: 1px solid @color_divider; + display: flex; + + span, a { + white-space: nowrap; + } + + span { + margin-right: 10px; } - .layout-wrapper { - background: @color_main_background; - padding-bottom: 5px; + a { + margin: 0px 6px; + font-weight: 500; + margin-right: 20px; } } diff --git a/src/styles/header.less b/src/styles/header.less index 2d993d7a..462c7b83 100644 --- a/src/styles/header.less +++ b/src/styles/header.less @@ -1,15 +1,10 @@ header { - position: fixed; - top: 0; width: 100%; - height: 50px; - background: #060606; - color: #f6f7f8; - overflow: hidden; - min-width: 780px; - z-index: 100; + height: 52px; + background: @color_header; + svg { - fill: #ffffff; + fill: @color_white; height: 28px; width: 100px; margin-top: 9px; @@ -22,4 +17,11 @@ header { margin-top: 16px; margin-left: 20px; } + + .layout-wrapper { + width: 100%; + max-width: 1170px; + margin: 0 auto; + padding: 0 15px; + } } diff --git a/src/styles/index.less b/src/styles/index.less index 72eb2ab7..a58f3cb6 100644 --- a/src/styles/index.less +++ b/src/styles/index.less @@ -3,11 +3,26 @@ @import './mixins.less'; @import './base.less'; @import './header.less'; +@import './aside.less'; +@import './content.less'; @import './footer.less'; -@import './devices.less'; @import './modal.less'; -@import './accounts.less'; + +@import './reactSelect.less'; +@import './rcTooltip.less'; + @import './history.less'; @import './send.less'; -@import './receive.less'; \ No newline at end of file +@import './receive.less'; +@import './summary.less'; +@import './signverify.less'; + +@import './landingPage.less'; + +@import './dashboard.less'; +@import './acquire.less'; +@import './notification.less'; + +@import './inputs.less'; +@import './loader.less'; diff --git a/src/styles/inputs.less b/src/styles/inputs.less new file mode 100644 index 00000000..84748d61 --- /dev/null +++ b/src/styles/inputs.less @@ -0,0 +1,226 @@ +input, textarea { + font-size: 14px; + font-weight: 300; + line-height: 1.42857143; + font-family: @font-family-monospace; + color: @color_text_primary; + background-color: @color_white; + border: 1px solid @color_divider; + border-radius: 2px; + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + padding: 6px 12px; + + &:focus { + box-shadow: 0 1px 2px 0 rgba(169, 169, 169, 0.25); + } + + &:disabled { + background: @color_gray_light; + color: @color_text_secondary; + } +} + +input { + + &.valid { + border-color: @color_success_primary; + &:focus { + box-shadow: 0 1px 4px 0 rgba(1, 183, 87, 0.25); + } + } + + &.warning { + border-color: @color_warning_primary; + &:focus { + box-shadow: 0 1px 4px 0 rgba(1, 183, 87, 0.25); + } + } + + &.not-valid { + border-color: @color_error_primary; + &:focus { + box-shadow: 0 1px 4px 0 rgba(255, 111, 109, 0.25); + } + } +} + + +button { + padding: 12px 24px; + border-radius: 3px; + font-size: 14px; + font-weight: 300; + cursor: pointer; + background: @color_green_primary; + color: @color_white; + border: 0px; + + .hover(); + + &:hover { + background: @color_green_secondary; + } + + &:active { + background: @color_green_tertiary; + } + + &:disabled { + pointer-events: none; + color: @color_text_secondary; + background: @color_gray_light; + } + + &.blue { + background: transparent; + border: 1px solid @color_info_primary; + color: @color_info_primary; + padding: 12px 58px; + + &:hover { + color: @color_white; + background: @color_info_primary; + } + } + + &.white { + background: @color_white; + color: @color_text_secondary; + border: 1px solid @color_divider; + &:hover { + //color: @color_text_primary; + //border-color: @color_text_primary; + background: @color_divider; + } + &:active { + color: @color_text_primary; + background: @color_divider; + } + } + + &.transparent { + background: transparent; + border: 0px; + color: @color_text_secondary; + .hover(); + + &:hover, + &:active { + color: @color_text_primary; + background: transparent; + } + } +} + + +.custom-checkbox { + + position: relative; + display: flex; + align-items: center; + cursor: pointer; + color: @color_text_secondary; + + input { + position: absolute; + left: -9999px; + z-index: -1; + opacity: 0; + + &:checked + .indicator:after { + background-color: @color_green_primary; + border-color: @color_green_primary; + } + &:disabled + .indicator:after { + background-color: @color_text_secondary; + } + } + + .indicator { + position: relative; + height: 24px; + width: 24px; + margin-right: 12px; + + &:after { + .icomoon-checked; + .hover(); + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + color: @color_white; + background-color: @color_white; + border: 1px solid @color_divider; + border-radius: 2px; + } + + &:hover { + border-color: @color_text_secondary; + } + } + + &.radio { + .indicator { + &:after { + border-radius: 50%; + } + } + + input:checked + .indicator:after { + content: ''; + background: white; + border: 4px solid @color_green_primary; + } + } + + &.align-left { + padding-left: 20px; + padding-right: 10px; + .indicator { + position: absolute; + left: 0; + top: 2px; + margin: 0; + &:after { + top: 0px; + } + } + } +} + +a.green, +a.green:visited { + position: relative; + color: @color_green_primary; + .hover(); + + &:after { + content: ''; + position: absolute; + width: 100%; + border-top: 1px solid @color_green_primary; + line-height: 1px; + left: 0px; + bottom: -1px; + transition: border-color 0.3s; + } + + &:hover { + color: @color_green_secondary; + } + + &:active { + color: @color_green_tertiary; + } + + &:hover, + &:active { + &:after { + border-color: @color_white; + } + } + + +} diff --git a/src/styles/landingPage.less b/src/styles/landingPage.less new file mode 100644 index 00000000..31b80daa --- /dev/null +++ b/src/styles/landingPage.less @@ -0,0 +1,142 @@ +.app { + &.connect-device { + //min-height: 100vh; + // overflow: hidden; + background: @color_landing; + + main { + flex-direction: column; + text-align: center; + padding-top: 65px; + margin-top: 0px; + + h2.claim { + font-size: 36px; + padding-bottom: 24px; + } + + .row { + display: flex; + flex-direction: row; + justify-content: space-around; + padding: 36px 0px; + margin: 0 auto; + width: 720px; + + p { + // flex: 1; + align-self: center; + } + + // a { + // color: @color_green_primary; + // text-decoration: underline; + // font-weight: 500; + // .hover(); + // &:hover { + // text-decoration: none; + // color: @color_green_secondary; + // } + // } + } + + p { + color: @color_text_secondary; + line-height: 1.8; + + &.connect { + color: @color_green_primary; + font-size: 16px; + font-weight: 500; + + span { + vertical-align: top; + position: relative; + top: 1px; + left: 12px; + animation: pulsate 1.3s ease-out infinite; + position: relative; + + svg { + position: absolute; + top: -8px; + left: -24px; + } + } + } + } + + .image { + width: 100%; + height: calc(100vh - 143px); + min-height: 500px; + flex: 1; + background-image: url('../images/case.png'); + background-repeat: no-repeat; + background-position: center 0px; + background-size: contain; + } + + img { + width: 90%; + height: auto; + margin: auto; + } + } + + .connect-usb-pin, + .connect-usb-cable { + animation: connect 1.3s ease-out infinite; + } + + footer { + border: 0px; + justify-content: center; + width: 100%; + max-width: 1170px; + margin: 0px auto; + } + + .notification { + width: 100%; + max-width: 1170px; + margin: 0px auto; + } + } +} + + +.landing { + text-align: center; + position: relative; + min-height: 100vh; + background: @color_landing; + + .loader-circle { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + margin: auto; + } +} + +@keyframes pulsate { + 0%, 100% { + opacity: 0.5; + } + 50% { + opacity: 1.0; + } +} + +@keyframes connect { + 0%, 100% { + transform: translateY(0px); + } + 50% { + transform: translateY(-4px) + } +} + diff --git a/src/styles/loader.less b/src/styles/loader.less new file mode 100644 index 00000000..6b5338ae --- /dev/null +++ b/src/styles/loader.less @@ -0,0 +1,73 @@ +.loader-circle { + + position: relative; + width: 100px; + height: 100px; + display: flex; + justify-content: center; + align-items: center; + + p { + position: absolute; + //margin: auto; + //line-height: 100%; + color: @color_text_secondary; + } + + .circular { + + width: 100%; + height: 100%; + animation: rotate 2s linear infinite; + transform-origin: center center; + + position: absolute; + + .route { + stroke: @color_gray_light; + } + + .path { + stroke-dasharray: 1, 200; + stroke-dashoffset: 0; + animation: dash 1.5s ease-in-out infinite, color 6s ease-in-out infinite; + stroke-linecap: round; + } + } +} + +@keyframes rotate { + 100% { + transform: rotate(360deg); + } +} + +@keyframes dash { + 0% { + stroke-dasharray: 1, 200; + stroke-dashoffset: 0; + } + 50% { + stroke-dasharray: 89, 200; + stroke-dashoffset: -35; + } + 100% { + stroke-dasharray: 89, 200; + stroke-dashoffset: -124; + } +} + +@keyframes color { + 100%, 0% { + stroke: @color_green_primary; + } + 40% { + stroke: @color_green_primary; + } + 66% { + stroke: @color_green_secondary; + } + 80%, 90% { + stroke: @color_green_tertiary; + } +} \ No newline at end of file diff --git a/src/styles/mixins.less b/src/styles/mixins.less index 3bba0f50..76f2f1d0 100644 --- a/src/styles/mixins.less +++ b/src/styles/mixins.less @@ -9,4 +9,23 @@ border-color: inherit !important; -webkit-box-shadow: none !important; box-shadow: none !important; +} + +.hover() { + transition: background-color 0.3s ease-in-out, color 0.3s ease-in-out, border-color 0.3s ease-in-out; +} + +.placeholder(@rules) { + &::-webkit-input-placeholder { + @rules(); + } + &:-moz-placeholder { + @rules(); + } + &::-moz-placeholder { + @rules(); + } + &:-ms-input-placeholder { + @rules(); + } } \ No newline at end of file diff --git a/src/styles/modal.less b/src/styles/modal.less index ae323b96..d5509e98 100644 --- a/src/styles/modal.less +++ b/src/styles/modal.less @@ -5,136 +5,257 @@ height: 100%; top: 0px; left: 0px; - //display: none; - - &.opened { - display: block; - } + background: rgba(0, 0, 0, 0.35); + display: flex; + flex-direction: column; + align-items: center; + overflow: auto; + padding: 20px; .modal-window { - margin: 20px auto; - padding: 20px; - overflow-x: hidden; - overflow-y: auto; + margin: auto; + // padding: 24px 48px; position: relative; - max-width: 100%; - border-radius: 6px; - box-sizing: border-box; - box-shadow: 0px 16px 16px 8px rgba(0, 0, 0, 0.1); - width: 500px; - min-height: 350px; - background-color: #ffffff; + border-radius: 4px; + background-color: @color_white; text-align: center; + overflow: hidden; } - .pin { - h4 { - margin-bottom: 10px; + h3 { + color: @color_text_primary; + font-size: 16px; + font-weight: 500; + margin-top: 14px; + } + p { + margin: 5px 0px; + font-weight: normal; + color: @color_text_secondary; + font-size: 12px; + } + + .confirm-tx { + width: 390px; // address overflow + + .header { + padding: 24px 48px; + &:before { + .icomoon-T1; + font-size: 52px; + color: @color_text_secondary; + } + h3 { + margin: 0; + } + } + .content { + border-top: 1px solid @color_divider; + background: @color_main; + padding: 24px 48px; + + label { + font-size: 10px; + color: @color_text_secondary; + } + + p { + font-size: 12px; + font-weight: 400; + color: @color_text_primary; + } + } + } + + .confirm-address { + width: 390px; // address overflow + + .header { + padding: 24px 48px; + &:before { + .icomoon-T1; + font-size: 52px; + color: @color_text_secondary; + } + h3 { + margin: 0; + } + } + .content { + border-top: 1px solid @color_divider; + background: @color_main; + padding: 24px 48px; + + label { + font-size: 12px; + color: @color_text_secondary; + } + + p { + font-size: 12px; + font-weight: 400; + color: @color_text_primary; + } + } + } + + .confirm-address-unverified { + width: 370px; + padding: 24px 48px; + button:not(.close-modal) { + width: 100%; + margin-top: 12px; + } + } + + .remember { + width: 360px; + padding: 24px 48px; + + p { + padding: 14px 0px; + } + + button { + width: 100%; + margin-top: 12px; + span { + position: relative; + } } - .pin_row { + + .loader-circle { + position: absolute; + top: 0; + bottom: 0; + left: -36px; + margin: auto; + p { + margin: 0; + padding: 0; + color: @color_text_secondary; + } + } + } + + .close-modal { + position: absolute; + top: 0; + right: 0; + padding: 12px; + + &:after { + .icomoon-close; + } + } + + .pin { + padding: 24px 48px; + .pin-row { button { - width: 55px; - height: 55px; - margin-top: 10px; - margin-left: 5px; + width: 80px; + height: 80px; + margin-top: 15px; + margin-left: 10px; + color: @color_text_primary; + border: 1px solid @color_divider; + background: @color_white; + transition: all 0.3s; &:first-child { margin-left: 0px; } + &:hover { + color: @color_text_primary; + border-color: @color_text_secondary; + } + &:active { + color: @color_text_primary; + background: @color_divider; + border-color: @color_divider; + } + } } - .pin_input_row { - margin-top: 10px; + .pin-input-row { + margin-top: 24px; display: inline-block; position: relative; } input { - width: 185px; + letter-spacing: 6px; + line-height: 48px; + font-weight: bold; + font-size: 18px; + height: auto; + padding: 0px 34px; + color: @color_text_primary; + background: transparent; } - .pin_backspace { + .pin-backspace { position: absolute; - right: 0; - top: 4px; - padding: 3px 3px 1px; - border: 0; - border-radius: 0; - &:active { - color: @color_link; - background: transparent; + right: 14px; + top: 0; + bottom: 0; + margin: auto 0; + padding: 0; + &:after { + .icomoon-arrow-left; } } - .submit { - margin-top: 10px; - width: 185px; + a { + color: @color_green_primary; } } .passphrase { - h4 { - margin-bottom: 10px; - } - label { - display: block; - } - .passphrase_options { - margin-top: 10px; - margin-bottom: 10px; - span { - font-size: 14px; - color: @color_secondary_text; + padding: 24px 48px; + .row { + position: relative; + text-align: left; + padding-top: 24px; + label:not(.custom-checkbox) { + display: block; + padding-bottom: 6px; + color: @color_text_secondary; } - } - - button { - width: 250px; - } + + .error { + position: absolute; + left: 0px; + bottom: -19px; + font-size: 12px; + color: @color_error_primary; + } + } + + // input[type="text"] { + // font-family: 'fontello'; + // font-size: 6px; + // line-height: 14px; + // } } input[type="text"], input[type="password"] { - width: 250px; - padding: 6px; - border: 0; - border-bottom: 1px solid @color_secondary_text; - background: transparent; - color: @color_primary_text; - font-size: 18px; - // disable lastpass icons - background-image: none !important; - padding-right: 0 !important; - } + width: 260px; + box-shadow: none; + border-radius: 0px; + border: 1px solid @color_divider; + height: auto; - button { - padding: 8px 12px; - font-size: 18px; - background: transparent; - border: 1px solid @color_secondary_text; - border-radius: 5px; - color: @color_secondary_text; - cursor: pointer; - transition: background 0.5s, color 0.5s, border 0.5s, opacity 0.5s; - - &:hover { - color: @color_link; - border-color: @color_link; - } - &:active { - color: @color_white; - border-color: @color_secondary_text; - background: @color_secondary_text; - transition: none; - } - &:focus { - outline: 0; - } + .placeholder({ + color: @color_divider; + }); } - - button[disabled] { - opacity: 0.8; - pointer-events: none; + + .submit { + width: 100%; + margin-top: 24px; + margin-bottom: 14px; } } diff --git a/src/styles/notification.less b/src/styles/notification.less new file mode 100644 index 00000000..16dd3006 --- /dev/null +++ b/src/styles/notification.less @@ -0,0 +1,126 @@ +.notification { + position: relative; + color: @color_info_primary; + background: @color_info_secondary; + padding: 24px 48px 24px 80px; + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-between; + text-align: left; + + .notification-body { + flex: 1; + margin-right: 24px; + } + + .notification-action button { + padding: 12px 58px; + } + + .notification-close { + position: absolute; + top: 8px; + right: 0; + padding: 12px; + color: inherit; + transition: opacity 0.3s; + + &:after { + .icomoon-close; + } + &:active, + &:hover { + opacity: 0.6; + color: inherit; + } + } + + h2 { + font-size: 14px; + font-weight: bold; + + padding: 0px; + &:before { + .icomoon-info; + position: absolute; + top: 17px; + left: 40px; + font-size: 32px !important; + } + } + + p { + padding: 0px; + margin-bottom: 8px 0px; + color: inherit; + } + + &.info { + .notification-action button { + border: 1px solid @color_info_primary; + color: @color_info_primary; + &:hover { + color: @color_white; + background: @color_info_primary; + } + } + } + + + + &.success { + color: @color_success_primary; + background: @color_success_secondary; + + .notification-action button { + border: 1px solid @color_success_primary; + color: @color_success_primary; + &:hover { + color: @color_white; + background: @color_success_primary; + } + } + } + + &.warning { + color: @color_warning_primary; + background: @color_warning_secondary; + h2:before { + .icomoon-warning; + } + + .notification-action button { + border: 1px solid @color_warning_primary; + color: @color_warning_primary; + &:hover { + color: @color_white; + background: @color_warning_primary; + } + } + } + + &.error { + color: @color_error_primary; + background: @color_error_secondary; + h2:before { + .icomoon-error; + } + + .notification-close { + color: @color_error_primary; + &:hover { + color: @color_error_primary; + } + } + + .notification-action button { + border: 1px solid @color_error_primary; + color: @color_error_primary; + &:hover { + color: @color_white; + background: @color_error_primary; + } + } + } +} \ No newline at end of file diff --git a/src/styles/rcTooltip.less b/src/styles/rcTooltip.less new file mode 100644 index 00000000..d2a2c5e8 --- /dev/null +++ b/src/styles/rcTooltip.less @@ -0,0 +1,147 @@ +.tooltip-wrapper { + width: 320px; + font-size: 10px; + span { + color: @color_green_primary; + } +} + +.rc-tooltip { + position: absolute; + z-index: 1070; + display: block; + visibility: visible; + border: 1px solid @color_divider; + border-radius: 3px; + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.06); +} + +.rc-tooltip-hidden { + display: none; +} + +.rc-tooltip-inner { + padding: 8px 10px; + color: @color_text_secondary; + font-size: 12px; + line-height: 1.5; + text-align: left; + text-decoration: none; + background-color: @color_white; + border-radius: 3px; + min-height: 34px; + border: 1px solid @color_white; +} +.rc-tooltip-arrow, +.rc-tooltip-arrow-inner { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + + +.rc-tooltip-placement-top .rc-tooltip-arrow, +.rc-tooltip-placement-topLeft .rc-tooltip-arrow, +.rc-tooltip-placement-topRight .rc-tooltip-arrow { + bottom: -6px; + margin-left: -6px; + border-width: 6px 6px 0; + border-top-color: @color_divider; +} +.rc-tooltip-placement-top .rc-tooltip-arrow-inner, +.rc-tooltip-placement-topLeft .rc-tooltip-arrow-inner, +.rc-tooltip-placement-topRight .rc-tooltip-arrow-inner { + //bottom: 1px; + bottom: 2px; + margin-left: -6px; + border-width: 6px 6px 0; + border-top-color: @color_white; +} +.rc-tooltip-placement-top .rc-tooltip-arrow { + left: 50%; +} +.rc-tooltip-placement-topLeft .rc-tooltip-arrow { + left: 15%; +} +.rc-tooltip-placement-topRight .rc-tooltip-arrow { + right: 15%; +} +.rc-tooltip-placement-right .rc-tooltip-arrow, +.rc-tooltip-placement-rightTop .rc-tooltip-arrow, +.rc-tooltip-placement-rightBottom .rc-tooltip-arrow { + left: -5px; + margin-top: -6px; + border-width: 6px 6px 6px 0; + border-right-color: @color_divider; +} +.rc-tooltip-placement-right .rc-tooltip-arrow-inner, +.rc-tooltip-placement-rightTop .rc-tooltip-arrow-inner, +.rc-tooltip-placement-rightBottom .rc-tooltip-arrow-inner { + left: 1px; + margin-top: -6px; + border-width: 6px 6px 6px 0; + border-right-color: @color_white; +} +.rc-tooltip-placement-right .rc-tooltip-arrow { + top: 50%; +} +.rc-tooltip-placement-rightTop .rc-tooltip-arrow { + top: 15%; + margin-top: 0; +} +.rc-tooltip-placement-rightBottom .rc-tooltip-arrow { + bottom: 15%; +} +.rc-tooltip-placement-left .rc-tooltip-arrow, +.rc-tooltip-placement-leftTop .rc-tooltip-arrow, +.rc-tooltip-placement-leftBottom .rc-tooltip-arrow { + right: -5px; + margin-top: -6px; + border-width: 6px 0 6px 6px; + border-left-color: @color_divider; +} +.rc-tooltip-placement-left .rc-tooltip-arrow-inner, +.rc-tooltip-placement-leftTop .rc-tooltip-arrow-inner, +.rc-tooltip-placement-leftBottom .rc-tooltip-arrow-inner { + right: 1px; + margin-top: -6px; + border-width: 6px 0 6px 6px; + border-left-color: @color_white; +} +.rc-tooltip-placement-left .rc-tooltip-arrow { + top: 50%; +} +.rc-tooltip-placement-leftTop .rc-tooltip-arrow { + top: 15%; + margin-top: 0; +} +.rc-tooltip-placement-leftBottom .rc-tooltip-arrow { + bottom: 15%; +} +.rc-tooltip-placement-bottom .rc-tooltip-arrow, +.rc-tooltip-placement-bottomLeft .rc-tooltip-arrow, +.rc-tooltip-placement-bottomRight .rc-tooltip-arrow { + top: -5px; + margin-left: -6px; + border-width: 0 6px 6px; + border-bottom-color: @color_divider; +} +.rc-tooltip-placement-bottom .rc-tooltip-arrow-inner, +.rc-tooltip-placement-bottomLeft .rc-tooltip-arrow-inner, +.rc-tooltip-placement-bottomRight .rc-tooltip-arrow-inner { + top: 1px; + margin-left: -6px; + border-width: 0 6px 6px; + border-bottom-color: @color_white; +} +.rc-tooltip-placement-bottom .rc-tooltip-arrow { + left: 50%; +} +.rc-tooltip-placement-bottomLeft .rc-tooltip-arrow { + left: 15%; +} +.rc-tooltip-placement-bottomRight .rc-tooltip-arrow { + right: 15%; +} diff --git a/src/styles/reactSelect.less b/src/styles/reactSelect.less new file mode 100644 index 00000000..56f7beb2 --- /dev/null +++ b/src/styles/reactSelect.less @@ -0,0 +1,329 @@ +// https://github.com/JedWatson/react-select/blob/master/less/select.less + +@import '~react-select/less/select'; + +// override predefined colors +@select-primary-color: @color_white; +@select-input-hover-box-shadow: none; +@select-input-box-shadow-focus: transparent; +@select-input-border-radius: 0px; +@select-item-border-radius: 0px; +@select-input-border-color: transparent; +@select-input-border-focus: @color_divider; + +.Select-focus-state(@color) { + // do nothing + background: transparent; + box-shadow: none; +} + +.Select-focus-state-classic() { + background: transparent; + box-shadow: none; +} + +.Select-arrow-zone { + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + padding-right: 0px !important; + width: 24px; + height: 24px; + right: 8px; + + .Select-arrow { + top: 0px; + border: 0px; + width: 24px; + + &:after { + .icomoon-arrow-down; + transition: transform 0.3s, color 0.3s; + color: @color_text_secondary; + transform-origin: 50% 50%; + font-size: 24px; + } + } +} + +.Select { + + .Select-control { + cursor: pointer; + + .Select-input { + background: transparent; + position: absolute; + top: 0; + // display: none !important; // uncomment for disable auto closing + } + + &:hover { + .Select-arrow:after { + color: @color_text_primary; + } + } + } + + .Select-noresults { + + } + + .Select-value-label { + color: @color_text_primary; + } + + .Select-menu-outer { + border-radius: 0px; + border: 1px solid @color_divider; + box-shadow: none; + } + + &.is-open { + .Select-arrow { + top: 0px !important; + border: 0px; + &:after { + transform: rotate(180deg); + } + } + } +} + + + + + + + + + + +/*@select-input-height: 34px; +@select-primary-color: #fff; +@select-input-bg-focus: #ff0000; +@select-input-border-radius: 0px; +@select-input-border-focus: @select-input-border-color; + +.Select { + width: 240px; + height: 34px; + display: inline-block; + vertical-align: middle; + + &.is-focused:not(.is-open) > .Select-control { + border-color: @select-input-border-color; + box-shadow: none; + } +} + +.Select-control { + &:hover { + box-shadow: none; + } +} + + + + + +.Select-menu-outer { + border: 1px solid rgba(0, 0, 0, 0.15); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); +} + +.Select-option { + &:last-child { + border-radius: 0px; + } + + .fee-label { + display: inline-block; + width: 70%; + } + .fee-size { + display: inline-block; + text-align: right; + } +} + +.CurrencySelect { + width: 70px; + vertical-align: top; +} + + +.CoinSelect { + width: 290px; + height: 64px; + .Select-control { + height: 63px; + border: 0px; + border-radius: 4px 0px 0px 0px; + border-right: 1px solid @color_divider; + cursor: pointer; + transition: all 0.2s ease-in-out; + + .Select-input { + background: transparent; + //display: none !important; + } + + &:hover { + background: #F2F2F2; + .Select-arrow { + &:after { + color: #494949; + } + } + } + } + + .Select-value { + padding: 0px; + .Select-value-label { + display: inline-block; + height: 63px; + padding-top: 20px; + padding-left: 50px; + font-size: 1.15em; + font-weight: bold; + line-height: 26px; + color: #494949; + + } + &:before { + content: ''; + position: absolute; + display: block; + width: 20px; + height: 20px; + z-index: 2; + left: 20px; + top: 21px; + + + background-image: url(../images/eth-logo.png); + background-repeat: no-repeat; + background-position: center; + background-size: auto 20px; + } + } + + .Select-menu-outer { + position: relative; + top: 0; + border: 0px; + border-top: 1px solid rgba(218, 218, 218, 0.5); + border-right: 1px solid rgba(218, 218, 218, 0.5); + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.04); + max-height: none; + } + + .Select-menu { + max-height: none; + overflow-x: none; + } + + .Select-option { + width: 290px; + height: 64px; + padding-top: 20px; + padding-left: 60px; + position: relative; + transition: all 0.2s ease-in-out; + + span { + height: 63px; + font-size: 1.15em; + font-weight: bold; + line-height: 26px; + color: #494949; + } + + &:before { + content: ''; + position: absolute; + display: block; + width: 20px; + height: 20px; + z-index: 2; + left: 20px; + top: 21px; + background-repeat: no-repeat; + background-position: center; + background-size: 20px 20px; + } + + &.btc:before { + background-image: url('../images/btc-logo.png'); + } + &.ltc:before { + background-image: url('../images/ltc-logo.png'); + } + &.btg:before { + background-image: url('../images/btg-logo.png'); + } + &.bch:before { + background-image: url('../images/bch-logo.png'); + } + &.dash:before { + background-image: url('../images/dash-logo.png'); + } + &.zec:before { + background-image: url('../images/zec-logo.png'); + } + &.eth:before { + background-image: url('../images/eth-logo.png'); + background-size: auto 20px; + } + &.etc:before { + background-image: url('../images/etc-logo.png'); + background-size: auto 20px; + } + + &:hover { + background: #F2F2F2; + } + + &.is-selected { + background: yellow; + } + } + + .Select-arrow-zone { + width: 28px; + } + + .Select-arrow { + border: 0px; + width: 28px; + + &:after { + .glyphicon-down; + color: #B3B3B3; + position: absolute; + left: 0px; + top: -8px; + transition: all 0.2s ease-in-out; + } + } + + &.is-open { + .Select-arrow { + top: 0px; + &:after { + .glyphicon-up; + } + } + } +} + + + +// /* +// +// +// +// */ \ No newline at end of file diff --git a/src/styles/receive.less b/src/styles/receive.less index 8418144a..ec6642a9 100644 --- a/src/styles/receive.less +++ b/src/styles/receive.less @@ -1,8 +1,94 @@ .receive { - flex: 1; - padding: 10px; - display: flex; - flex-direction: column; - border: 1px solid red; + .address { + position: relative; + padding: 0px 48px; + display: flex; + flex-wrap: wrap; + + .value { + // same as input (inputs.less) + font-size: 14px; + font-weight: 300; + line-height: 1.42857143; + font-family: @font-family-monospace; + color: @color_text_primary; + border: 1px solid @color_divider; + border-radius: 3px; + padding: 6px 12px; + padding-right: 38px; // eye icon + position: relative; + flex: 1; + user-select: all; /* Chrome and Opera */ + } + + button { + padding: 6px 24px; + + &.white { + padding: 0px; + border: 0px; + position: absolute; + height: 100%; + background: transparent; + right: 48px; + } + + span { + display: flex; + align-items: center; + white-space: nowrap; + &:before { + .icomoon-eye; + font-size: 32px; + line-height: 14px; + // padding-top: 2px; + padding-right: 4px; + } + } + + } + + &.hidden { + .value { + padding-right: 6px; // no eye icon + user-select: none; + border-radius: 3px 0px 0px 3px; + &:after { + content: ''; + position: absolute; + left: 0; + top: 0; + height: 100%; + width: 100%; + background: linear-gradient(to right, + rgba(255,255,255, 0) 0%, + rgba(255,255,255, 1) 220px + ); + pointer-events: none; /* so the text is still selectable */ + } + } + + button { + border-radius: 0px 3px 3px 0px; + } + } + + &.unverified { + button span:before { + .icomoon-eye-error; + color: @color_error_primary; + font-size: 32px; + line-height: 14px; + padding-top: 0px; + padding-right: 4px; + } + } + + + } + + .qr { + margin: 24px 48px; + } } \ No newline at end of file diff --git a/src/styles/send.less b/src/styles/send.less index c50da008..094516e2 100644 --- a/src/styles/send.less +++ b/src/styles/send.less @@ -1,101 +1,286 @@ -.address-menu { - a { - padding: 10px; - } -} - .send-form { - flex: 1; - padding: 10px; - display: flex; - flex-direction: column; + padding-bottom: 24px; - border: 1px solid red; + .Select { + width: 98px; + height: 34px; + font-family: @font-family-monospace; + + &.fee { + width: 100%; + } + + .Select-control { + height: 34px; + border: 1px solid @color_divider; + border-radius: 0px 2px 2px 0px; + } + + .Select-option { + .hover(); + &.is-focused { + background: @color_gray_light; + } + + &.is-selected { + background: @color_divider; + } + } + + .fee-option { + display: flex; + align-items: center; + + .fee-value { + flex: 1; + color: @color_text_primary; + } + + .fee-label { + color: @color_text_secondary; + font-size: 12px; + font-weight: 400; + padding-right: 36px; + } + } + + } .row { + position: relative; display: block; - padding-bottom: 10px; - } + padding: 0px 48px; + padding-bottom: 24px; - label { - display: inline-block; - text-transform: uppercase; - &:first-child { - width: 20%; - padding-right: 10px; - text-align: right; + .error, + .warning, + .info { + position: absolute; + left: 48px; + bottom: 6px; + font-size: 12px; + color: @color_error_primary; } + + .error { + color: @color_error_primary; + } + .warning { + color: @color_warning_primary; + } + .info { + color: @color_green_primary; + } + } + + .input-icon { + position: absolute; + top: 0; + bottom: 0; + right: 54px; + margin: auto 0; + height: 26px; + color: @color_green_primary; } - input, select, textarea { - display: inline-block; - color: #424242; - font-size: 14px; - padding: 6px 12px; - line-height: 1; - background-color: #fff; - border: 1px solid #ccc; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - outline: 0; + .address-input { + input.valid + .input-icon:before { + .icomoon-checked; + } + input.not-valid + .input-icon:before { + .icomoon-error; + color: @color_error_primary; + } + input.warning + .input-icon:before { + .icomoon-warning; + color: @color_warning_primary; + } } - input[type=text] { - transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; - width: 400px; - &.small { - width: 150px; + .amount-input { + position: relative; + display: flex; + flex-direction: row; + + input { + flex: 1; + border-radius: 2px 0px 0px 2px; } - &:disabled { - background: #dddddd; + + .set-max { + position: relative; + height: 34px; + line-height: 34px; + font-size: 12px; + font-weight: 300; // different + color: @color_text_secondary; + border: 1px solid @color_divider; + border-right: 0px; + border-left: 0px; + background: @color_white; + padding: 0px 10px 0px 32px; + cursor: pointer; + .hover(); + + &:before { + .icomoon-setmax; + width: 24px; + height: 24px; + font-size: 24px; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + left: 4px; + } + + &:hover { + background: @color_gray_light; + } + + &:active { + background: @color_divider; + } + + &.enabled { + color: @color_white; + background: @color_green_primary; + &:before { + .icomoon-checked; + } + + &:hover { + background: @color_green_secondary; + } + + &:active { + background: @color_green_tertiary; + } + } + } + } + + .advanced { + font-weight: 500; + line-height: 40px; // button height + color: @color_text_secondary; + .hover(); + &:hover, + &:active { + color: @color_text_primary; + } + &:after { + .icomoon-arrow-down; + transition: transform 0.3s; + transform-origin: 50% 50%; + font-size: 24px; + position: relative; + top: 6px; + left: 8px; } } + .advanced-container { + display: flex; + justify-content: space-between; + padding: 0px 48px; + button { + width: 50%; + } + &.opened { + flex-direction: column; + padding: 0px; + button { + position: relative; + left: 50%; + width: 50%; + } + .advanced { + display: inline-block; + margin: 0px 48px 12px 48px; + &:after { + transform: rotate(180deg); + top: 5px; + } + } + } - select { - background: url('data:image/svg+xml;utf8,'); - background-color: @color_white; - background-repeat: no-repeat; - background-position: right 8px top 6px; - background-size: 16px 16px; - width: 150px; - height: 31px; - -webkit-appearance: none; - -moz-appearance: none; - text-indent: 0px; - text-overflow: ''; - border-radius: 0; - &:disabled { - background: #dddddd; + .what-is-it { + &:before { + .icomoon-help; + .hover(); + transform-origin: 50% 50%; + font-size: 24px; + position: relative; + top: 5px; + cursor: pointer; + } + &:hover { + &:before { + color: @color_text_primary; + } + } } + } - button { - display: inline-block; - font-weight: normal; - text-align: center; - vertical-align: middle; - cursor: pointer; - border: 1px solid transparent; - white-space: nowrap; - padding: 6px 12px; - font-size: 14px; - font-weight: 600; - text-transform: uppercase; - line-height: 1.42857143; - border-radius: 0; - user-select: none; - transition: all 0.15s ease 0s; - color: #ffffff; - background-color: #4cc148; + .gas-row { + display: flex; + flex-direction: row; + border-top: 1px solid @color_divider; + padding-top: 24px; + + .column { + position: relative; + flex: 1; + padding-right: 20px; - &:hover { - box-shadow: 0 0 24px rgba(0, 0, 0, 0.15); + &:last-child { + padding-right: 0px; + } + + .error, + .warning, + .info { + left: 0; + bottom: -17px; + } } + } - &:disabled { - color: #666666; - pointer-events: none; - background: #dddddd; + .update-fee-levels { + position: relative; + font-size: 12px; + color: @color_warning_primary; + padding-left: 24px; + margin-left: 8px; + a { + text-decoration: underline; + color: @color_green_primary; + margin-left: 4px; + } + &:before { + .icomoon-warning; + position: absolute; + top: -4px; + left: 0; } } + + label { + display: block; + font-size: 14px; + color: @color_text_secondary; + padding-bottom: 4px; + } + + input, + textarea { + width: 100%; + } + + textarea { + resize: none; + height: 80px; + } } \ No newline at end of file diff --git a/src/styles/signverify.less b/src/styles/signverify.less new file mode 100644 index 00000000..356da153 --- /dev/null +++ b/src/styles/signverify.less @@ -0,0 +1,36 @@ +.signverify { + flex: 1; + display: flex; + flex-direction: row; + background: @color_white; + + h2 { + line-height: 74px; + } + + .sign, + .verify { + flex: 1; + display: flex; + flex-direction: column; + padding-left: 50px; + + textarea { + resize: vertical; + width: 100%; + } + + // textarea[readonly] { + // background: #A9A9A9; + // } + } + + label { + color: #A9A9A9; + padding: 5px 0px; + } + + .verify { + padding-right: 20px; + } +} \ No newline at end of file diff --git a/src/styles/summary.less b/src/styles/summary.less new file mode 100644 index 00000000..79bd50cc --- /dev/null +++ b/src/styles/summary.less @@ -0,0 +1,232 @@ +.summary { + + h2 { + //padding: 35px 50px 0px 50px; + color: red; + } + + .token-select { + width: 100%; + height: 34px; + font-family: @font-family-monospace; + + .Select-control { + height: 34px; + border: 1px solid @color_divider; + } + + .Select-input { + + } + + .Select-arrow-zone { + display: none; + } + } + + .identicon { + display: inline-block; + vertical-align: middle; + position: relative; + top: -4px; + margin-right: 10px; + border-radius: 50%; + } + + + .summary-details { + position: relative; + padding: 35px 50px 0px 50px; + border-bottom: 1px solid @color_divider; + + .content { + .column { + display: inline-block; + width: 25%; + padding-bottom: 30px; + + .label { + color: #A9A9A9; + font-weight: 600; + } + + .fiat-value { + font-weight: bold; + font-size: 1.2em; + margin: 7px 0 6px 0; + color: #494949; + } + } + } + + .toggle { + display: block; + position: absolute; + left: 50%; + margin-left: -20px; + bottom: -20px; + width: 40px; + height: 40px; + //line-height: 30px; + background: @color_white; + color: #B3B3B3; + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.04); + border-radius: 50%; + cursor: pointer; + transition: all 0.2s ease-in-out; + + &:hover { + background: #F2F2F2; + &:before { + color: #494949; + } + } + + &:before { + .glyphicon-up; + color: #B3B3B3; + position: absolute; + left: 14px; + top: 16px; + transition: all 0.2s ease-in-out; + } + } + + &.closed { + .content { + display: none; + } + .toggle { + &:before { + .glyphicon-down; + top: 18px; + } + } + } + } + + .filter { + background: @color_main; + padding: 30px 48px 10px 48px; + // text-align: right; + + // input { + // width: 300px; + // } + } + + .add-token-form { + position: relative; + .toggle { + cursor: pointer; + padding: 15px 50px; + } + + .content { + display: flex; + flex-direction: row; + padding: 15px 50px; + } + + + .column { + padding-right: 10px; + label { + display: block; + color: #A9A9A9; + font-weight: 600; + } + + input { + &.token-address { + width: 230px; + } + + &.token-name { + width: 160px; + } + + &.token-shortcut { + width: 80px; + } + + &.token-decimal { + width: 80px; + } + } + + button { + + } + } + + &:after { + .glyphicon-up; + color: #B3B3B3; + position: absolute; + right: 50px; + top: 21px; + transition: all 0.2s ease-in-out; + } + + &:hover { + &:after { + color: #494949; + } + } + + &.closed { + &:after { + .glyphicon-down; + } + } + } + + .token { + border-top: 1px solid @color_divider; + padding: 15px 50px; + display: flex; + flex-direction: row; + + .icon { + width: 36px; + height: 36px; + //border: 8px solid white; + border-radius: 50%; + margin-right: 10px; + line-height: 30px; + text-transform: uppercase; + user-select: none; + text-align: center; + padding: 6px; + p { + line-height: 24px; + padding: 0px; + color: inherit; + } + } + + .name { + flex: 1; + line-height: 30px; + } + + .balance { + color: red; + line-height: 30px; + } + + &:last-child { + // border-bottom: 1px solid @color_divider; + } + } + + .token-select { + .Select-control { + cursor: text; + } + } +} + + + diff --git a/src/styles/topNavigation.less b/src/styles/topNavigation.less new file mode 100644 index 00000000..0003ecb1 --- /dev/null +++ b/src/styles/topNavigation.less @@ -0,0 +1,243 @@ +nav { + display: flex; + width: 100%; + max-width: 1170px; + height: 64px; + margin: 0 auto; + margin-top: 32px; + z-index: 1; + background: @color_white; + border-radius: 4px 4px 0px 0px; + border-bottom: 1px solid @color_divider; + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.04); + + @media screen and (max-width: 1170px) { + border-radius: 0px; + margin-top: 0px; + } + + // .layout-wrapper { + // height: 100%; + // background: @color_white; + // border-radius: 4px 4px 0px 0px; + // border-bottom: 1px solid rgba(218, 218, 218, 0.5); + // box-shadow: 0 3px 8px rgba(0, 0, 0, 0.04); + // } + + // override styles for react-select + .device-select { + width: 320px; + height: 64px; + // display: inline-block; + // vertical-align: middle; + box-shadow: none; + + &.is-focused:not(.is-open) > .Select-control { + border-color: @color_divider; + box-shadow: none; + } + + .Select-control { + height: 63px; + border: 0px; + border-radius: 4px 0px 0px 0px; + border-right: 1px solid @color_divider; + cursor: pointer; + transition: color 0.2s ease-in-out; + + .Select-input { + background: transparent; + position: absolute; + top: 0; + //display: none !important; + } + + &:hover { + background: transparent; + // border: 0px; + border-right: 1px solid @color_divider; + box-shadow: none; + + .Select-arrow { + &:after { + color: @color_text_primary; + } + } + } + } + + .Select-arrow-zone { + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + padding: 0px; + width: 24px; + height: 24px; + right: 24px; + + .Select-arrow { + top: 0px; + border: 0px; + width: 24px; + + &:after { + .icomoon-arrow-down; + transition: transform 0.3s; + color: @color_text_secondary; + transform-origin: 50% 50%; + font-size: 24px; + } + } + } + + + + .Select-option { + &:hover { + background: red; + } + + &.is-selected { + background: yellow; + } + } + + &.is-open { + .Select-control { + border-color: @color_divider; + } + + .Select-arrow { + top: 0px !important; + border: 0px; + + + &:after { + // .icomoon-arrow-up; + transform: rotate(180deg); + + } + } + } + + &.is-disabled { + + .Select-control { + background: transparent; + cursor: default; + } + + .Select-arrow { + visibility: hidden; + &:after { + content: '' + } + } + + .device { + .device-menu { + padding-right: 24px; + } + } + } + + .Select-menu-outer { + border-radius: 0px; + border: 1px solid @color_divider; + box-shadow: none; + visibility: hidden; + } + } + + .device { + height: 63px; + width: 319px; + display: flex; + align-items: center; + padding-left: 80px; + + &:before { + content: ''; + position: absolute; + display: block; + width: 13px; + height: 25px; + z-index: 2; + left: 33px; + top: 17px; + background-repeat: no-repeat; + background-position: center; + background-size: 13px 25px; + background-image: url('../images/icontrezor.png'); + } + + .label-container { + flex: 1; + overflow: hidden; + span { + display: block; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + + &.label { + font-weight: 500; + font-size: 14px; + color: @color_text_primary; + } + + &.status { + font-size: 12px; + color: @color_text_secondary; + } + } + } + + .device-menu { + display: flex; + justify-content: flex-end; + padding-right: 48px; + padding-left: 4px; + + div { + display: inline-block; + } + + .forget, + .settings, + .acquire { + cursor: pointer; + + &:before { + .icomoon-refresh; + color: @color_text_secondary; + position: relative; + font-size: 24px; + .hover(); + } + + &:hover { + &:before { + color: @color_text_primary; + } + } + } + + .forget { + &:before { + .icomoon-eject; + } + } + + .settings { + &:before { + .icomoon-settings; + } + } + } + + } + + +} diff --git a/webpack/webpack.config.dev.js b/webpack/webpack.config.dev.js index 6ea5578a..f50c2f65 100644 --- a/webpack/webpack.config.dev.js +++ b/webpack/webpack.config.dev.js @@ -38,6 +38,15 @@ module.exports = { fallback: 'style-loader' }) }, + { + test: /\.css$/, + loader: extractLess.extract({ + use: [ + { loader: 'css-loader' } + ], + fallback: 'style-loader' + }) + }, { test: /\.(png|gif|jpg)$/, loader: 'file-loader?name=./images/[name].[ext]' @@ -50,14 +59,14 @@ module.exports = { }, }, { - test: /\.json$/, + test: /\.json($|\?)/, loader: 'json-loader' }, { test: /\.(ttf|eot|svg|woff|woff2)$/, loader: 'file-loader', query: { - name: '[name].[ext]', + name: './fonts/[name].[hash].[ext]', }, }, @@ -71,6 +80,7 @@ module.exports = { }, plugins: [ extractLess, + new HtmlWebpackPlugin({ chunks: ['index'], template: `${SRC}index.html`, @@ -91,7 +101,8 @@ module.exports = { new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('development'), PRODUCTION: JSON.stringify(false) - }) + }), + new webpack.IgnorePlugin(/node-fetch/), // for trezor-link warning ], // ignoring "fs" import in fastxpub node: { diff --git a/webpack/webpack.config.prod.babel.js b/webpack/webpack.config.prod.babel.js index b8fb76eb..1d02592c 100644 --- a/webpack/webpack.config.prod.babel.js +++ b/webpack/webpack.config.prod.babel.js @@ -1,4 +1,4 @@ -import { SRC, BUILD } from './constants'; +import { SRC, BUILD, TREZOR_LIBRARY, TREZOR_CONNECT_FILES } from './constants'; import webpack from 'webpack'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import ExtractTextPlugin from 'extract-text-webpack-plugin'; @@ -11,10 +11,11 @@ const extractLess = new ExtractTextPlugin({ module.exports = { entry: { - index: ['whatwg-fetch', `${SRC}js/index.js`] + index: ['whatwg-fetch', `${SRC}js/index.js`], + 'trezor-library': `${TREZOR_LIBRARY}.js` }, output: { - filename: 'js/[name].[chunkhash].js', + filename: 'js/[name].[hash].js', path: BUILD }, module: { @@ -35,18 +36,41 @@ module.exports = { fallback: 'style-loader' }) }, + { + test: /\.css$/, + loader: extractLess.extract({ + use: [ + { loader: 'css-loader' } + ], + fallback: 'style-loader' + }) + }, + { + test: /\.(png|gif|jpg)$/, + loader: 'file-loader?name=../images/[name].[ext]' + }, { test: /\.(ttf|eot|svg|woff|woff2)$/, loader: 'file-loader?publicPath=../&name=fonts/[name].[ext]', }, { - test: /\.(png|gif|jpg)$/, - loader: 'file-loader?publicPath=../&name=images/[name].[ext]', + test: /\.(wasm)$/, + loader: 'file-loader', + query: { + name: 'js/[name].[ext]', + }, + }, + { + test: /\.json$/, + loader: 'json-loader' }, ] }, resolve: { - modules: [SRC, 'node_modules'] + modules: [SRC, 'node_modules'], + alias: { + 'trezor-connect': `${TREZOR_LIBRARY}`, + } }, performance: { hints: false @@ -62,22 +86,32 @@ module.exports = { new CopyWebpackPlugin([ //{from: `${SRC}/app/robots.txt`}, - { from: `${SRC}js/vendor`, to: `${BUILD}js/vendor` }, - { from: `${SRC}images/favicon.ico` }, - { from: `${SRC}images/favicon.png` }, + //{ from: `${SRC}js/vendor`, to: `${BUILD}js/vendor` }, //{ from: `${SRC}config.json` }, { from: `${SRC}images`, to: `${BUILD}images` }, + { from: `${SRC}data`, to: `${BUILD}data` }, ]), new webpack.optimize.OccurrenceOrderPlugin(), new webpack.NoEmitOnErrorsPlugin(), - new webpack.optimize.UglifyJsPlugin({ - compress: { - warnings: false, - } - }), + // new webpack.optimize.UglifyJsPlugin({ + // compress: { + // warnings: false, + // } + // }), + new CopyWebpackPlugin([ + { from: `${TREZOR_CONNECT_FILES}coins.json` }, + { from: `${TREZOR_CONNECT_FILES}releases.json` }, + { from: `${TREZOR_CONNECT_FILES}latest.txt` }, + { from: `${TREZOR_CONNECT_FILES}config_signed.bin` }, + ]), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production'), PRODUCTION: JSON.stringify(false) - }) - ] + }), + new webpack.IgnorePlugin(/node-fetch/), // for trezor-link warning + ], + // ignoring "fs" import in fastxpub + node: { + fs: "empty" + } } diff --git a/yarn.lock b/yarn.lock index 4545209a..110f1adf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -27,6 +27,12 @@ acorn@^5.0.0: version "5.2.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.2.1.tgz#317ac7821826c22c702d66189ab8359675f135d7" +add-dom-event-listener@1.x: + version "1.0.2" + resolved "https://registry.yarnpkg.com/add-dom-event-listener/-/add-dom-event-listener-1.0.2.tgz#8faed2c41008721cf111da1d30d995b85be42bed" + dependencies: + object-assign "4.x" + ajv-keywords@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762" @@ -773,7 +779,7 @@ babel-register@^6.26.0: mkdirp "^0.5.1" source-map-support "^0.4.15" -babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0: +babel-runtime@6.x, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" dependencies: @@ -846,6 +852,10 @@ big.js@^3.1.3: version "3.2.0" resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" +bignumber.js@^2.3.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-2.4.0.tgz#838a992da9f9d737e0f4b2db0be62bb09dd0c5e8" + bignumber.js@^4.0.2: version "4.1.0" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-4.1.0.tgz#db6f14067c140bd46624815a7916c92d9b6c24b1" @@ -1109,7 +1119,7 @@ clap@^1.0.9: dependencies: chalk "^1.1.3" -classnames@^2.2.5: +classnames@^2.2.4, classnames@^2.2.5: version "2.2.5" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d" @@ -1170,6 +1180,10 @@ color-convert@^1.3.0, color-convert@^1.9.0: dependencies: color-name "^1.1.1" +color-hash@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/color-hash/-/color-hash-1.0.3.tgz#c0e7952f06d022e548e65da239512bd67d3809ee" + color-name@^1.0.0, color-name@^1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" @@ -1218,6 +1232,16 @@ commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" +component-classes@^1.2.5: + version "1.2.6" + resolved "https://registry.yarnpkg.com/component-classes/-/component-classes-1.2.6.tgz#c642394c3618a4d8b0b8919efccbbd930e5cd691" + dependencies: + component-indexof "0.0.3" + +component-indexof@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/component-indexof/-/component-indexof-0.0.3.tgz#11d091312239eb8f32c8f25ae9cb002ffe8d3c24" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -1306,6 +1330,14 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: safe-buffer "^5.0.1" sha.js "^2.4.8" +create-react-class@^15.6.0: + version "15.6.2" + resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.2.tgz#cf1ed15f12aad7f14ef5f2dfe05e6c42f91ef02a" + dependencies: + fbjs "^0.8.9" + loose-envify "^1.3.1" + object-assign "^4.1.1" + cross-spawn@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -1340,6 +1372,13 @@ crypto-js@^3.1.4: version "3.1.8" resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-3.1.8.tgz#715f070bf6014f2ae992a98b3929258b713f08d5" +css-animation@^1.3.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/css-animation/-/css-animation-1.4.1.tgz#5b8813125de0fbbbb0bbe1b472ae84221469b7a8" + dependencies: + babel-runtime "6.x" + component-classes "^1.2.5" + css-color-names@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" @@ -1511,6 +1550,10 @@ diffie-hellman@^5.0.0: miller-rabin "^4.0.0" randombytes "^2.0.0" +dom-align@1.x: + version "1.6.7" + resolved "https://registry.yarnpkg.com/dom-align/-/dom-align-1.6.7.tgz#6858138efb6b77405ce99146d0be5e4f7282813f" + dom-converter@~0.1: version "0.1.4" resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.1.4.tgz#a45ef5727b890c9bffe6d7c876e7b19cb0e17f3b" @@ -1745,6 +1788,12 @@ ethereumjs-tx@^1.3.3: ethereum-common "^0.0.18" ethereumjs-util "^5.0.0" +ethereumjs-units@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/ethereumjs-units/-/ethereumjs-units-0.2.0.tgz#6ea31132aabc2cc7b8a5290e265593a337687fa3" + dependencies: + bignumber.js "^2.3.0" + ethereumjs-util@^5.0.0, ethereumjs-util@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-5.1.2.tgz#25ba0215cbb4c2f0b108a6f96af2a2e62e45921f" @@ -1877,7 +1926,7 @@ fastparse@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8" -fbjs@^0.8.16: +fbjs@^0.8.16, fbjs@^0.8.9: version "0.8.16" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db" dependencies: @@ -2616,6 +2665,10 @@ lodash-es@^4.2.0, lodash-es@^4.2.1: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.4.tgz#dcc1d7552e150a0640073ba9cb31d70f032950e7" +lodash._getnative@^3.0.0: + version "3.9.1" + resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" + lodash._reinterpolate@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" @@ -2624,6 +2677,22 @@ lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" +lodash.isarguments@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + +lodash.isarray@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" + +lodash.keys@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" + dependencies: + lodash._getnative "^3.0.0" + lodash.isarguments "^3.0.0" + lodash.isarray "^3.0.0" + lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -2944,7 +3013,7 @@ oauth-sign@~0.8.1: version "0.8.2" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" -object-assign@^4.0.0, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: +object-assign@4.x, object-assign@^4.0.0, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -3085,6 +3154,10 @@ path-to-regexp@^1.7.0: dependencies: isarray "0.0.1" +path-to-regexp@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.1.0.tgz#7e30f9f5b134bd6a28ffc2e3ef1e47075ac5259b" + path-type@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" @@ -3105,6 +3178,10 @@ performance-now@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + pify@^2.0.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -3402,7 +3479,7 @@ promise@^7.1.1: dependencies: asap "~2.0.3" -prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.0: +prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.0: version "15.6.0" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856" dependencies: @@ -3474,6 +3551,12 @@ querystring@0.2.0, querystring@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" +raf@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575" + dependencies: + performance-now "^2.1.0" + randomatic@^1.1.3: version "1.1.7" resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c" @@ -3507,6 +3590,50 @@ raw-body@2.3.2: iconv-lite "0.4.19" unpipe "1.0.0" +rc-align@2.x: + version "2.3.5" + resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-2.3.5.tgz#5085cfa4d685ee9d030b9afd2971eb370c5e80a1" + dependencies: + babel-runtime "^6.26.0" + dom-align "1.x" + prop-types "^15.5.8" + rc-util "^4.0.4" + +rc-animate@2.x: + version "2.4.4" + resolved "https://registry.yarnpkg.com/rc-animate/-/rc-animate-2.4.4.tgz#a05a784c747beef140d99ff52b6117711bef4b1e" + dependencies: + babel-runtime "6.x" + css-animation "^1.3.2" + prop-types "15.x" + +rc-tooltip@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/rc-tooltip/-/rc-tooltip-3.7.0.tgz#3afbf109865f7cdcfe43752f3f3f501f7be37aaa" + dependencies: + babel-runtime "6.x" + prop-types "^15.5.8" + rc-trigger "^2.2.2" + +rc-trigger@^2.2.2: + version "2.3.4" + resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-2.3.4.tgz#389dfa5e834ecc3a446fe9cefc0b4a32900f4227" + dependencies: + babel-runtime "6.x" + prop-types "15.x" + rc-align "2.x" + rc-animate "2.x" + rc-util "^4.4.0" + +rc-util@^4.0.4, rc-util@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-4.4.0.tgz#f6a320a67100cfceaaa1b0a955b01e9be643576c" + dependencies: + add-dom-event-listener "1.x" + babel-runtime "6.x" + prop-types "^15.5.10" + shallowequal "^0.2.2" + rc@^1.1.7: version "1.2.2" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.2.tgz#d8ce9cb57e8d64d9c7badd9876c7c34cbe3c7077" @@ -3516,10 +3643,41 @@ rc@^1.1.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-blockies@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/react-blockies/-/react-blockies-1.2.2.tgz#36f2a1aa8b1e43012d0007396b3d0ac83e21807f" + dependencies: + prop-types "^15.5.10" + +react-css-transition@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/react-css-transition/-/react-css-transition-0.7.4.tgz#546682a6eac87c98f6333c49cbf2dd995a93de36" + dependencies: + react-transition-group "^1.0.0" + reassemble "^0.5.6" + react-deep-force-update@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/react-deep-force-update/-/react-deep-force-update-2.1.1.tgz#8ea4263cd6455a050b37445b3f08fd839d86e909" +react-dom@^15.4.1: + version "15.6.2" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.6.2.tgz#41cfadf693b757faf2708443a1d1fd5a02bef730" + dependencies: + fbjs "^0.8.9" + loose-envify "^1.1.0" + object-assign "^4.1.0" + prop-types "^15.5.10" + +"react-dom@^15.4.2 || ^16.0.0": + version "16.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.2.0.tgz#69003178601c0ca19b709b33a83369fe6124c044" + dependencies: + fbjs "^0.8.16" + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.0" + react-dom@^16.1.1: version "16.1.1" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.1.1.tgz#b2e331b6d752faf1a2d31399969399a41d8d45f8" @@ -3529,6 +3687,13 @@ react-dom@^16.1.1: object-assign "^4.1.1" prop-types "^15.6.0" +react-ellipsis-text@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/react-ellipsis-text/-/react-ellipsis-text-1.0.0.tgz#98ac5d4e1a2b21e7f76e49f5f23ce65c99f74590" + dependencies: + react "^15.4.1" + react-dom "^15.4.1" + react-hot-loader@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-3.1.3.tgz#6f92877326958c7cb0134b512474517869126082" @@ -3539,6 +3704,12 @@ react-hot-loader@^3.1.3: redbox-react "^1.3.6" source-map "^0.6.1" +react-input-autosize@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.1.2.tgz#a3dc11a5517c434db25229925541309de3f7a8f5" + dependencies: + prop-types "^15.5.8" + react-proxy@^3.0.0-alpha.0: version "3.0.0-alpha.1" resolved "https://registry.yarnpkg.com/react-proxy/-/react-proxy-3.0.0-alpha.1.tgz#4400426bcfa80caa6724c7755695315209fa4b07" @@ -3594,6 +3765,35 @@ react-router@^4.2.0: prop-types "^15.5.4" warning "^3.0.0" +react-scale-text@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/react-scale-text/-/react-scale-text-1.2.2.tgz#4a56e1d2fd4e4582d2ad472c003ee12f51cbf2ae" + dependencies: + lodash "^4.17.4" + prop-types "^15.6.0" + react "^15.4.2 || ^16.0.0" + react-dom "^15.4.2 || ^16.0.0" + shortid "^2.2.8" + warning "^3.0.0" + +react-select@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/react-select/-/react-select-1.1.0.tgz#626a2de839fdea2ade74dd1b143a9bde34be6c82" + dependencies: + classnames "^2.2.4" + prop-types "^15.5.8" + react-input-autosize "^2.1.0" + +react-transition-group@^1.0.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-1.2.1.tgz#e11f72b257f921b213229a774df46612346c7ca6" + dependencies: + chain-function "^1.0.0" + dom-helpers "^3.2.0" + loose-envify "^1.3.1" + prop-types "^15.5.6" + warning "^3.0.0" + react-transition-group@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.2.1.tgz#e9fb677b79e6455fd391b03823afe84849df4a10" @@ -3605,6 +3805,25 @@ react-transition-group@^2.2.1: prop-types "^15.5.8" warning "^3.0.0" +react@^15.4.1: + version "15.6.2" + resolved "https://registry.yarnpkg.com/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72" + dependencies: + create-react-class "^15.6.0" + fbjs "^0.8.9" + loose-envify "^1.1.0" + object-assign "^4.1.0" + prop-types "^15.5.10" + +"react@^15.4.2 || ^16.0.0": + version "16.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba" + dependencies: + fbjs "^0.8.16" + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.0" + react@^16.1.1: version "16.1.1" resolved "https://registry.yarnpkg.com/react/-/react-16.1.1.tgz#d5c4ef795507e3012282dd51261ff9c0e824fe1f" @@ -3659,6 +3878,12 @@ readdirp@^2.0.0: readable-stream "^2.0.2" set-immediate-shim "^1.0.1" +reassemble@^0.5.6: + version "0.5.6" + resolved "https://registry.yarnpkg.com/reassemble/-/reassemble-0.5.6.tgz#0162c769ff3d5a25a15c6296b0c4653c0014d8e0" + dependencies: + fbjs "^0.8.9" + redbox-react@^1.3.6: version "1.5.0" resolved "https://registry.yarnpkg.com/redbox-react/-/redbox-react-1.5.0.tgz#04dab11557d26651bf3562a67c22ace56c5d3967" @@ -3932,6 +4157,12 @@ sha.js@^2.4.0, sha.js@^2.4.8: inherits "^2.0.1" safe-buffer "^5.0.1" +shallowequal@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-0.2.2.tgz#1e32fd5bcab6ad688a4812cb0cc04efc75c7014e" + dependencies: + lodash.keys "^3.1.2" + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -3942,6 +4173,10 @@ shebang-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" +shortid@^2.2.8: + version "2.2.8" + resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.8.tgz#033b117d6a2e975804f6f0969dbe7d3d0b355131" + signal-exit@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"