diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +node_modules/ diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..6b30f1a --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,162 @@ +{ + "env": { + "browser": true, + "es6": true, + "node": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 2020 + }, + "rules": { + "indent": ["error", 2], + "linebreak-style": ["error", "unix"], + "quotes": [ + "error", + "single", + { + "allowTemplateLiterals": true + } + ], + "semi": ["error", "always"], + "no-loop-func": ["error"], + "block-spacing": ["error", "always"], + "camelcase": ["error"], + "eqeqeq": ["error", "always"], + "strict": ["error", "global"], + "brace-style": [ + "error", + "1tbs", + { + "allowSingleLine": true + } + ], + "comma-style": ["error", "last"], + "comma-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "eol-last": ["error"], + "func-call-spacing": ["error", "never"], + "key-spacing": [ + "error", + { + "beforeColon": false, + "afterColon": true, + "mode": "minimum" + } + ], + "keyword-spacing": [ + "error", + { + "before": true, + "after": true, + "overrides": { + "function": { + "after": false + } + } + } + ], + "max-len": [ + "error", + { + "code": 80, + "ignoreUrls": true + } + ], + "max-nested-callbacks": [ + "error", + { + "max": 7 + } + ], + "new-cap": [ + "error", + { + "newIsCap": true, + "capIsNew": false, + "properties": true + } + ], + "new-parens": ["error"], + "no-lonely-if": ["error"], + "no-trailing-spaces": ["error"], + "no-unneeded-ternary": ["error"], + "no-whitespace-before-property": ["error"], + "object-curly-spacing": ["error", "always"], + "operator-assignment": ["error", "always"], + "operator-linebreak": ["error", "after"], + "semi-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "space-before-blocks": ["error", "always"], + "space-before-function-paren": [ + "error", + { + "anonymous": "never", + "named": "never", + "asyncArrow": "always" + } + ], + "space-in-parens": ["error", "never"], + "space-infix-ops": ["error"], + "space-unary-ops": [ + "error", + { + "words": true, + "nonwords": false, + "overrides": { + "typeof": false + } + } + ], + "no-unreachable": ["error"], + "no-global-assign": ["error"], + "no-self-compare": ["error"], + "no-unmodified-loop-condition": ["error"], + "no-constant-condition": [ + "error", + { + "checkLoops": false + } + ], + "no-console": ["off"], + "no-useless-concat": ["error"], + "no-useless-escape": ["error"], + "no-shadow-restricted-names": ["error"], + "no-use-before-define": [ + "error", + { + "functions": false + } + ], + "arrow-parens": ["error", "as-needed"], + "arrow-body-style": ["error", "as-needed"], + "arrow-spacing": ["error"], + "no-confusing-arrow": [ + "error", + { + "allowParens": true + } + ], + "no-useless-computed-key": ["error"], + "no-useless-rename": ["error"], + "no-var": ["error"], + "object-shorthand": ["error", "always"], + "prefer-arrow-callback": ["error"], + "prefer-const": ["error"], + "prefer-numeric-literals": ["error"], + "prefer-rest-params": ["error"], + "prefer-spread": ["error"], + "rest-spread-spacing": ["error", "never"], + "template-curly-spacing": ["error", "never"] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..74a670a --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Project exclude paths +/.idea/ +/node_modules/ +tsconfig.json +*.txt diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..74d3eb4 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "singleQuote": true, + "trailingComma": "es5", + "overrides": [ + { + "files": ".prettierrc", + "options": { "parser": "json" } + } + ] +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..70093e9 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +https://t.me/MrPaschenko. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..34d87c4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,3 @@ +If you have any ideas of how to improve the program, feel free to add new Issues on [Issues page](https://github.com/MrPaschenko/op-coursework/issues). + +You can also create Pull Requests on [Pull Requests page](https://github.com/MrPaschenko/op-coursework/pulls), if you are famliar with JavaScript and Node.js. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..231e7f4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (C) 2021 Dmytro Pashchenko, Ivan Labiak, Andrii Vostrikov, Vladyslav Yaroshchuk + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b77b1ee --- /dev/null +++ b/README.md @@ -0,0 +1,338 @@ +# OP Coursework +This little program is dedicated to work with any kinds of currency: cryptocurrencies (like BTC, ETH etc.) or the usual ones (USD, EUR etc.). + +## Installation +1. Clone the repository: +```bash +$ git clone https://github.com/readme-experts/op-coursework +``` +2. Open folder: +```bash +$ cd op-coursework +``` +3. Run the program: +```bash +$ node index.js +``` +This app has no dependencies so you don't have to install them via npm. + +## 1. Currency to BTC exchange rate +Get BTC exchange rate to any currency you want just by entering its [code](https://www.iban.com/currency-codes). +``` +Select action +>1 +Type currency you want to convert +>usd +USD: 53986.32 +``` +``` +Select action +>1 +Type currency you want to convert +>eur +EUR: 45036.28 +``` +_Credits to [CryptoCompare](https://www.cryptocompare.com/)_ + +## 2. Top five crypto by volume +Get top 5 cryptocurrencies by its volume. +``` +Select action +>2 +1. Ethereum +2. Dogecoin +3. Bitcoin +4. XRP +5. Bitcoin Cash +``` +_Credits to [CryptoCompare](https://www.cryptocompare.com/)_ + +## 3. Currency 24 h volume +Get pair exchange rate for past 24 hours by entering two currency codes splitting them by coma, but not using space (like `usd,btc`). +``` +Select action +>3 +Type curr you want to get 24h volume of/res curr +usd,btc +The lowest price for 24 hours is: 0.00001747 btc +The highest price for 24 hours is: 0.00001877 btc +24 hour price differance: 0.00 btc +``` +_Credits to [CryptoCompare](https://www.cryptocompare.com/)_ + +## 4. Exchange rates of UAH from bank.gov.ua +Get official exchange rates. +``` +Select action +>4 +┌─────────┬──────────┬────────────────┬───────────────┐ +│ (index) │ currency │ exchangeAmount │ exchangeRate │ +├─────────┼──────────┼────────────────┼───────────────┤ +│ 0 │ 'AUD' │ 1 │ '21,6242 UAH' │ +│ 1 │ 'BYN' │ 10 │ '10,8526 UAH' │ +│ 2 │ 'BGN' │ 1 │ '17,204 UAH' │ +│ 3 │ 'KRW' │ 100 │ '2,5053 UAH' │ +│ 4 │ 'HKD' │ 1 │ '3,5746 UAH' │ +│ 5 │ 'DKK' │ 1 │ '4,5246 UAH' │ +│ 6 │ 'USD' │ 1 │ '27,75 UAH' │ +│ 7 │ 'EUR' │ 1 │ '33,6427 UAH' │ +│ 8 │ 'EGP' │ 1 │ '1,7719 UAH' │ +│ 9 │ 'JPY' │ 10 │ '2,5453 UAH' │ +│ 10 │ 'PLN' │ 1 │ '7,3664 UAH' │ +│ 11 │ 'INR' │ 10 │ '3,748 UAH' │ +│ 12 │ 'CAD' │ 1 │ '22,5812 UAH' │ +│ 13 │ 'HRK' │ 1 │ '4,4529 UAH' │ +│ 14 │ 'MXN' │ 1 │ '1,3887 UAH' │ +│ 15 │ 'MDL' │ 1 │ '1,5563 UAH' │ +│ 16 │ 'ILS' │ 1 │ '8,5482 UAH' │ +│ 17 │ 'NZD' │ 1 │ '20,1368 UAH' │ +│ 18 │ 'NOK' │ 1 │ '3,3928 UAH' │ +│ 19 │ 'ZAR' │ 10 │ '1,9469 UAH' │ +│ 20 │ 'RUB' │ 10 │ '3,7355 UAH' │ +│ 21 │ 'RON' │ 1 │ '6,8345 UAH' │ +│ 22 │ 'IDR' │ 1000 │ '1,9182 UAH' │ +│ 23 │ 'SAR' │ 1 │ '7,3992 UAH' │ +│ 24 │ 'SGD' │ 1 │ '20,9323 UAH' │ +│ 25 │ 'XDR' │ 1 │ '39,8933 UAH' │ +│ 26 │ 'KZT' │ 100 │ '6,4823 UAH' │ +│ 27 │ 'TRY' │ 1 │ '3,3738 UAH' │ +│ 28 │ 'HUF' │ 100 │ '9,3302 UAH' │ +│ 29 │ 'GBP' │ 1 │ '38,7015 UAH' │ +│ 30 │ 'CZK' │ 1 │ '1,3028 UAH' │ +│ 31 │ 'SEK' │ 1 │ '3,3261 UAH' │ +│ 32 │ 'CHF' │ 1 │ '30,5163 UAH' │ +│ 33 │ 'CNY' │ 1 │ '4,2887 UAH' │ +└─────────┴──────────┴────────────────┴───────────────┘ +``` +_Credits to [National Bank of Ukraine](https://bank.gov.ua/)_ + +## 5. Create wallet on BlockCypher +#### WARNING! Experimental feature. +Create cryptocurrency wallet on [BlockCypher](https://www.blockcypher.com/). +``` +Select action +>5 + Choose which wallet do you want to make: + 1 - Bitcoin; + 2 - Ethereum; + 3 - Dogecoin; + Type anything to exit. +Select action +>1 +Wallet was successfully created! Your wallet data: +$$$$ +``` +_Credits to [BlockCypher](https://www.blockcypher.com/)_ + +## 6. BTC Address Balance +Get BTC address balance by using [BlockCypher](https://www.blockcypher.com/). +``` +Select action +>6 +Write the address you want to get balance of +>address (instance: 13oiPCfq5xMePgVRmBvoxa2nXYHue7XRgm ) +Total received: $ satoshis +Total send: $ satoshis +Balance: $ satoshis +``` +_Credits to [BlockCypher](https://www.blockcypher.com/)_ + +## 7. Exchange rates of UAH from monobank.ua +Get exchange rates from one of the most popular banks of Ukraine. +``` +Select action +>7 +┌─────────┬───────────────┬───────────────┬──────────────┬─────────┬──────────┬───────────┐ +│ (index) │ currencyCodeA │ currencyCodeB │ date │ rateBuy │ rateSell │ rateCross │ +├─────────┼───────────────┼───────────────┼──────────────┼─────────┼──────────┼───────────┤ +│ 0 │ 'USD' │ 'UAH' │ '15.5.2021' │ 27.5 │ 27.7001 │ │ +│ 1 │ 'EUR' │ 'UAH' │ '15.5.2021' │ 33.25 │ 33.6496 │ │ +│ 2 │ 'RUB' │ 'UAH' │ '15.5.2021' │ 0.36 │ 0.39 │ │ +│ 3 │ 'EUR' │ 'USD' │ '15.5.2021' │ 1.204 │ 1.218 │ │ +│ 4 │ 'PLN' │ 'UAH' │ '16.5.2021' │ 7.35 │ 7.5 │ 7.5 │ +... +``` +_Credits to [monobank](https://www.monobank.ua/)_ + +## 8. Recent crypto news +Read recent news crypto news. +```` +Select action +>8 + +Five most recent articles on cryptocurrency: +1. Puerto Rico’s Crypto Tax Haven Dreams are on the Rocks +2. Billionaire investor Bill Ackman says he wouldn’t invest in bitcoin because it is purely speculative +3. Biden's $6 Trillion Budget Could Fuel Inflation Fears And Bitcoin Gains +4. Bitcoin Challenges $40K Again Following Biden’s $6 Trillion Spending Budget Reveal +5.Crypto Mouthpieces Need Muting: Why There’s No Coordinated Bitcoin FUD + +Enter number of article you'd like to read: +>1 + +Like the IRS in the mainland US, the Puerto Rican tax authority hasn’t issued definite guidance on crypto.The post Puerto Rico’s Crypto Tax Haven Dreams are on the Rocks appeared first on Blockworks. + +Clear menu(y/n)? +>choice +```` +Credits to [CryptoCompare](https://www.cryptocompare.com/) + +## 9. Exchange rates of UAH from privatbank.ua +Get exchange rates from one of the most popular banks of Ukraine. +``` +Select action +>9 +Do you want to get cash rate (1) or non-cash rate (2)? +1 +┌─────────┬───────┬──────────┬──────────────┬──────────────┐ +│ (index) │ ccy │ base_ccy │ buy │ sale │ +├─────────┼───────┼──────────┼──────────────┼──────────────┤ +│ 0 │ 'USD' │ 'UAH' │ '27.20000' │ '27.60000' │ +│ 1 │ 'EUR' │ 'UAH' │ '33.10000' │ '33.70000' │ +│ 2 │ 'RUR' │ 'UAH' │ '0.36000' │ '0.39000' │ +│ 3 │ 'BTC' │ 'USD' │ '35067.3591' │ '38758.6601' │ +└─────────┴───────┴──────────┴──────────────┴──────────────┘ +``` +Keep in mind that __there are two types of rate in PrivatBank__. +The first one is cash rate and the second one is non-cash rate. + +Usually non-cash rate is more profitable :) + +_Credits to [PrivatBank](https://privatbank.ua/)_ + +## 10. Cryptocurrency fee rates +Get cryptocurrency fee rates (Bitcoin, Bitcoin-Cash, Dogecoin, Dash, Litecoin). +``` +Select action +>10 +These are fee rates for some cryptocurrencies: +BITCOIN +fast: 0.00000208 +slow: 0.00000032 +standard: 0.00000073 + + +BITCOIN-CASH +fast: 0.00000004 +slow: 0.00000003 +standard: 0.00000004 + + +DOGECOIN +fast: 0.00523561 +slow: 0.00520833 +standard: 0.00522197 + + +DASH +fast: 0.00000005 +slow: 0.00000003 +standard: 0.00000004 + + +LITECOIN +fast: 0.00000026 +slow: 0.00000023 +standard: 0.00000025 + + +Clear menu(y/n)? +>n +``` + +## 11. Show currency code by its number and vice versa +Get currency number entering its code and currency code entering its number. +``` +Select action +>11 +Enter currency code or its number: +>usd +840 +``` +``` +Select action +>11 +Enter currency code or its number: +>980 +uah +``` +## 12. Transaction info by its hash +Find out information about any btc/dash/doge/ltc transaction by its hash. +```` +Select action +>12 + +List of cryptos: +1. Bitcoin +2. Dash +3. Dogecoin +4. Litecoin + +Enter the number of crypto from the list above you'd to like to input hash of: +>3 + +Enter the hash of transaction you'd like to get info about: +>645127c390c019f61b255ea79b15d8ebb5203e9febbe76b0a7531225e20275ff + +Satoshis sent: 14794235305186972 +Fee in satoshis: 100000000 +Transaction size in bytes: 226 +Transaction preference: high +Received at: 2021-05-27T15:23:38.019Z +Confirmed at: 2021-05-27T15:24:00Z +```` +Credits to [BlockCypher](https://www.blockcypher.com/) + +## 13. Exchange rates of UAH from bank.gov.ua (Alternative source) +Get official exchange rates. +``` +Select action +>13 +┌─────────┬──────┬──────────────────────────────────────┬────────────┬───────┬──────────────┐ +│ (index) │ r030 │ txt │ rate │ cc │ exchangedate │ +├─────────┼──────┼──────────────────────────────────────┼────────────┼───────┼──────────────┤ +│ 0 │ 36 │ 'Австралійський долар' │ 21.1396 │ 'AUD' │ '31.05.2021' │ +│ 1 │ 124 │ 'Канадський долар' │ 22.7079 │ 'CAD' │ '31.05.2021' │ +│ 2 │ 156 │ 'Юань Женьміньбі' │ 4.3126 │ 'CNY' │ '31.05.2021' │ +│ 3 │ 191 │ 'Куна' │ 4.4408 │ 'HRK' │ '31.05.2021' │ +│ 4 │ 203 │ 'Чеська крона' │ 1.3118 │ 'CZK' │ '31.05.2021' │ +│ 5 │ 208 │ 'Данська крона' │ 4.4899 │ 'DKK' │ '31.05.2021' │ +... +``` +_Credits to [National Bank of Ukraine](https://bank.gov.ua/)_ + +## Save results to .txt file +To the No. 1-3 there is also additional feature available: you can save results to text file by typing `1` after creating a request. +``` +Select action +>1 +Type currency you want to convert +>usd +USD: 54132.18 + +Print 1 to save results +>1 +Write the name of txt file to save your results +>btc to usd +``` +After that you can find file `btc to usd.txt` in root folder. It will contain next information: +``` +USD: 54132.18 +``` + +## Themes used in work +https://github.com/MrPaschenko/op-coursework/blob/master/src/themes.md + +## Authors +* Dmytro Pashchenko — https://github.com/MrPaschenko +* Ivan Labiak — https://github.com/ILabiak +* Andrii Vostrikov — https://github.com/NekoSuimin +* Vladyslav Yaroshchuk — https://github.com/thank1ess + +## Contributing +If you have any ideas of how to improve the program, feel free to add new Issues on [Issues page](https://github.com/MrPaschenko/op-coursework/issues). + +You can also create Pull Requests on [Pull Requests page](https://github.com/MrPaschenko/op-coursework/pulls), if you are famliar with JavaScript and Node.js. + +## License +This program is distributed under an [MIT License](https://github.com/MrPaschenko/op-coursework/blob/master/LICENSE). diff --git a/index.js b/index.js index 32ad9d6..feea0d2 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,57 @@ 'use strict'; -console.log('Hello, World!'); +const { Crypto } = require('./src/crypto.js'); +const { question } = require('./src/promised.js'); +const exchanges = require('./src/exchanges.js'); +const { colors } = require('./src/promised.js'); + +const crypto = new Crypto(); + +async function menu() { + console.log( + colors.green, + `Menu: + 1 - Currency to BTC exchange rate + 2 - Top five crypto by volume + 3 - Currency 24 h volume + 4 - Exchange rates of UAH from bank.gov.ua + 5 - Create wallet on BlockCypher + 6 - BTC Address Balance + 7 - monobank exchange rates + 8 - Recent Crypto News + 9 - PrivatBank exchange rates + 10 - Cryptocurrency fee rates + 11 - Show currency code by its number and vice versa + 12 - Transaction info by its hash + 13 - Exchange rates of UAH from bank.gov.ua (Alternative) + Type anything to exit.`, + colors.reset + ); + const selection = parseInt(await question('Select action\n')) - 1; + const features = [ + crypto.currencyToCrypto, + crypto.topFiveCurrencies, + crypto.currencyPriceVolume, + exchanges.nbuExchange, + exchanges.genWalletFeature, + exchanges.btcAdrBalance, + exchanges.monoExchange, + crypto.cryptoNews, + exchanges.privatExchange, + exchanges.feesRate, + exchanges.currencyCodeNumber, + exchanges.transactionInfo, + exchanges.nbuAlternative, + ]; + const bindfeatures = features.map(item => item.bind(crypto)); + if (bindfeatures[selection]) await bindfeatures[selection](); + else process.exit(); +} + +(async () => { + while (true) { + await menu(); + const answ = await question('Clear menu(y/n)?\n'); + if (answ === 'y') console.clear(); + } +})(); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..66f8767 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,33 @@ +{ + "name": "op-coursework", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "prettier": "^2.3.0" + } + }, + "node_modules/prettier": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.0.tgz", + "integrity": "sha512-kXtO4s0Lz/DW/IJ9QdWhAf7/NmPWQXkFr/r/WkR3vyI+0v8amTDxiaQSLzs8NBlytfLWX/7uQUMIW677yLKl4w==", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + } + } + }, + "dependencies": { + "prettier": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.0.tgz", + "integrity": "sha512-kXtO4s0Lz/DW/IJ9QdWhAf7/NmPWQXkFr/r/WkR3vyI+0v8amTDxiaQSLzs8NBlytfLWX/7uQUMIW677yLKl4w==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7a36fbe --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "op-coursework", + "version": "1.0.0", + "description": "Simple console application for getting data about currencies", + "main": "index.js", + "scripts": { + "start": "node index.js", + "test": "node ./src/tests.js", + "lint": "eslint . --fix && prettier --write \"**/*.js\" \"**/*.json\"" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/MrPaschenko/op-coursework.git" + }, + "author": "MrPaschenko, NekoSuimin, ILabiak, thank1ess", + "license": "MIT", + "bugs": { + "url": "https://github.com/MrPaschenko/op-coursework/issues" + }, + "homepage": "https://github.com/MrPaschenko/op-coursework#readme", + "devDependencies": { + "prettier": "^2.3.0" + } +} diff --git a/src/codesList.json b/src/codesList.json new file mode 100644 index 0000000..56bc43a --- /dev/null +++ b/src/codesList.json @@ -0,0 +1,116 @@ +{ + "8": "ALL", + "12": "DZD", + "32": "ARS", + "36": "AUD", + "48": "BHD", + "50": "BDT", + "51": "AMD", + "68": "BOB", + "72": "BWP", + "96": "BND", + "108": "BIF", + "116": "KHR", + "124": "CAD", + "144": "LKR", + "152": "CLP", + "156": "CNY", + "170": "COP", + "188": "CRC", + "191": "HRK", + "192": "CUP", + "203": "CZK", + "208": "DKK", + "230": "ETB", + "262": "DJF", + "270": "GMD", + "324": "GNF", + "344": "HKD", + "348": "HUF", + "352": "ISK", + "356": "INR", + "360": "IDR", + "364": "IRR", + "368": "IQD", + "376": "ILS", + "392": "JPY", + "398": "KZT", + "400": "JOD", + "404": "KES", + "408": "KPW", + "410": "KRW", + "414": "KWD", + "417": "KGS", + "418": "LAK", + "422": "LBP", + "434": "LYD", + "454": "MWK", + "458": "MYR", + "478": "MRO (MRU)", + "480": "MUR", + "484": "MXN", + "496": "MNT", + "498": "MDL", + "504": "MAD", + "512": "OMR", + "516": "NAD", + "524": "NPR", + "554": "NZD", + "558": "NIO", + "566": "NGN", + "578": "NOK", + "586": "PKR", + "600": "PYG", + "604": "PEN", + "608": "PHP", + "634": "QAR", + "643": "RUB", + "682": "SAR", + "690": "SCR", + "694": "SLL", + "702": "SGD", + "704": "VND", + "706": "SOS", + "710": "ZAR", + "748": "SZL", + "752": "SEK", + "756": "CHF", + "760": "SYP", + "764": "THB", + "784": "AED", + "788": "TND", + "795": "TMM (TMT)", + "800": "UGX", + "807": "MKD", + "818": "EGP", + "826": "GBP", + "834": "TZS", + "840": "USD", + "858": "UYU", + "860": "UZS", + "886": "YER", + "894": "ZMK (ZMW)", + "901": "TWD", + "933": "BYN", + "936": "GHS", + "938": "SDG", + "941": "RSD", + "943": "MZN", + "944": "AZN", + "946": "RON", + "949": "TRY", + "950": "XAF", + "952": "XOF", + "968": "SRD", + "969": "MGA", + "971": "AFN", + "972": "TJS", + "973": "AOA", + "975": "BGN", + "976": "CDF", + "978": "EUR", + "980": "UAH", + "981": "GEL", + "985": "PLN", + "986": "BRL" +} diff --git a/src/crypto.js b/src/crypto.js new file mode 100644 index 0000000..07b5eae --- /dev/null +++ b/src/crypto.js @@ -0,0 +1,126 @@ +'use strict'; + +const promised = require('./promised.js'); + +const errorHandlerWrapped = promised.errorWrapper(promised.handler); + +const safeGet = errorHandlerWrapped(promised.getRequest); +//const safeWrite = errorHandlerWrapped(promised.writeFile); + +class RawCrypto { + //#apiKey; + + constructor(/*key*/) { + this.defaultUrl = 'https://min-api.cryptocompare.com/data'; + //this.#apiKey = (key) ? key : null; + } + + async currencyToCrypto(currency) { + if (currency === undefined) { + currency = await promised.question('Type currency you want to convert\n'); + } + const query = this.defaultUrl + `/price?fsym=BTC&tsyms=${currency}`; + const result = await safeGet(query); + const resultText = []; + if (result) { + const keys = Object.keys(result); + for (const key of keys) { + resultText.push(`${key}: ${result[key]}`); + } + console.log(`${resultText.join('\n')}\n`); + //await safeWrite(resultText); + } + return resultText.join('\n'); + } + + async topFiveCurrencies() { + const query = this.defaultUrl + '/top/totalvolfull?limit=10&tsym=USD'; + const currencies = (await safeGet(query)).Data; + currencies.splice(4, 5); + const resultText = []; + const result = currencies.map(item => item.CoinInfo.FullName); + result.forEach((el, index) => { + resultText.push(`${index + 1}. ${el}`); + }); + console.log(`${resultText.join('\n')}\n`); + //await safeWrite(resultText); + return resultText.join('\n'); + } + + async currencyPriceVolume(input) { + if (input === undefined) { + const text = 'Type curr you want to get 24h volume of/res curr\n'; + input = await promised.question(text); + } + const [curr, volumeCurr] = input.split(', '); + const url = `/v2/histoday?fsym=${curr}&tsym=${volumeCurr}&limit=1`; + const query = this.defaultUrl + url; + const result = await safeGet(query); + const data = result.Data; + const resultText = []; + let priceDiff = data.Data[1].close - data.Data[0].close; + priceDiff = priceDiff.toFixed(2); + const lowest = `${data.Data[1].low} ${volumeCurr}`; + const highest = `${data.Data[1].high} ${volumeCurr} `; + let diff = `${priceDiff} ${volumeCurr}`; + if (result) { + const lowText = `The lowest price for 24 hours is: ${lowest}`; + const lowestText = promised.colors.red + lowText + promised.colors.green; + const highestText = `The highest price for 24 hours is: ${highest}`; + diff = priceDiff > 0 ? '+' + diff : diff; + const diffText = `24 hour price differance: ${diff}`; + resultText.push(lowestText, highestText, diffText); + + console.log(`${resultText.join('\n')}\n`); + //await safeWrite(resultText); + } + return resultText.join('\n'); + } + + async cryptoNews(writtenTitleNumber) { + const info = await safeGet(`${this.defaultUrl}/v2/news/?lang=EN`); + const data = info.Data; + const articleNumbers = [1, 2, 3, 4, 5]; + + const proposedTitles = + '\nFive most recent articles on cryptocurrency:\n' + + data + .filter((item, index) => index < articleNumbers.length) + .map( + (item, index) => `${index + 1}. ${promised.decodeString(item.title)}` + ) + .join('\n') + '\n'; + + if (writtenTitleNumber === undefined) { + console.log(proposedTitles); + writtenTitleNumber = await promised.question( + 'Enter number of article\'s title you\'d like to read:\n' + ); + } + + if (!articleNumbers.includes(+writtenTitleNumber)) { + throw new Error('Error inside function: "Wrong number"'); + } else if (data[writtenTitleNumber - 1].body === '') { + throw new Error('Error inside function: "API bug(body empty)"'); + } + + const fixedBody = promised.decodeString( + data[writtenTitleNumber - 1].body + ); + const result = `${writtenTitleNumber}. ` + + `${promised.decodeString(data[writtenTitleNumber - 1].title)}` + + `\n${fixedBody}`; + console.log(`\n${result}\n`); + return result; + } + + static from(key) { + return new RawCrypto(key); + } +} + +const Crypto = promised.classWrapper(RawCrypto, promised.classHandler); + +module.exports = { + Crypto, +}; diff --git a/src/exchanges.js b/src/exchanges.js new file mode 100644 index 0000000..dadc413 --- /dev/null +++ b/src/exchanges.js @@ -0,0 +1,244 @@ +'use strict'; + +const promised = require('./promised.js'); +const { Wallet } = require('./wallet.js'); +const codesList = require('./codesList.json'); + +const errorHandlerWrapped = promised.errorWrapper(promised.handler); + +const safeGet = errorHandlerWrapped(promised.getRequest); +const safePost = errorHandlerWrapped(promised.postRequest); +const safeSpawn = errorHandlerWrapped(promised.promiseSpawn); +//const safeWrite = errorHandlerWrapped(promised.writeFile); + +const genWalletFeature = async selection => { + if (selection === undefined) { + console.log( + '\x1b[32m', + `Choose which wallet do you want to make: + 1 - Bitcoin; + 2 - Ethereum; + 3 - Dogecoin; + Type anything to exit.` + ); + selection = parseInt(await promised.question('Select action\n')); + } + const currencies = ['btc', 'eth', 'doge']; + + if (!currencies[selection - 1]) { + throw new Error('Error inside function: "Wrong number"'); + } + + const resWall = currencies[selection - 1]; + const wallet = new Wallet(resWall, 'd190d4bbbc9e47a1962739eeb93f1819'); + await wallet.createWallet(); + console.log(`Wallet was successfully created! Your wallet data: + ${wallet.keys} + Please don't send anyone your private key or wif + or you'll loose your money. + We don't save any information about created wallets + Make sure you saved all the information. + `); + console.log(wallet.keys.join('\n')); + return wallet.keys.join('\n'); +}; + +const btcAdrBalance = async adrs => { + const wallet = new Wallet(); + if (adrs === undefined) { + console.log('Write the address you want to get balance of\n'); + adrs = await promised.question(''); + } + const res = await wallet.getAdrsBalance(adrs); + //await safeWrite(res); + return res.join('\n'); +}; + +const nbuExchange = async () => { + const data = await safeSpawn('python', './src/parser.py'); + console.table(data); +}; + +const nbuAlternative = async () => { + const data = await safeGet( + 'https://bank.gov.ua/NBUStatService/v1/statdirectory/exchange?json' + ); + console.table(data); + return data[0]['cc']; +}; + +const currencyCodeNumber = async request => { + if (request === undefined) { + const question = 'Enter currency code or its number:\n'; + request = await promised.question(question); + } + const error = + `${promised.colors.red}` + + 'Error inside function: "Wrong data"' + + `${promised.colors.reset}`; + + if (/^\d+$/.test(request)) { + const code = codesList[parseInt(request)]; + if (!code) { + throw new Error('Error inside function: "Not found"'); + } else { + console.log(code); + return code; + } + } else if (/[a-zA-Z]/.test(request)) { + let currency; + for (const curr in codesList) { + if (request.toUpperCase() === codesList[curr]) currency = curr; + } + if (currency) { + console.log(currency); + return +currency; + } + throw new Error('Error inside function: "Not found"'); + } else { + throw new Error(error); + } +}; + +const monoExchange = async () => { + const data = await safeGet('https://api.monobank.ua/bank/currency'); + if (data.errorDescription) { + throw new Error(data.errorDescription); + } + for (const curr of data) { + curr.currencyCodeA = codesList[curr.currencyCodeA]; + curr.currencyCodeB = codesList[curr.currencyCodeB]; + const rawDate = new Date(curr.date * 1000); + curr.date = + `${rawDate.getDate()}` + + `.${rawDate.getMonth() + 1}` + + `.${rawDate.getFullYear()}`; + } + console.table(data); + return data[0].currencyCodeA; +}; + +const privatExchange = async userChoice => { + const cash = await safeGet( + 'https://api.privatbank.ua/p24api/pubinfo?json&exchange&coursid=5' + ); + const nonCash = await safeGet( + 'https://api.privatbank.ua/p24api/pubinfo?exchange&json&coursid=11' + ); + const rateTypes = [cash, nonCash]; + + if (userChoice === undefined) { + const first = 'Do you want to get cash rate (1) or non-cash rate (2)?\n'; + userChoice = (await promised.question(first)); + } + + if (+userChoice !== 1 && +userChoice !== 2) { + throw new Error('Error inside function: "Wrong number"'); + } + console.table(rateTypes[userChoice - 1]); + return rateTypes[userChoice - 1][0]['buy']; +}; + +const feesRate = async () => { + const cryptos = ['bitcoin', 'bitcoin-cash', 'dogecoin', 'dash', 'litecoin']; + const res = []; + res.push('These are fee rates for some cryptocurrencies:'); + const options = { + method: 'GET', + hostname: 'rest.cryptoapis.io', + path: '', + qs: [], + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': '43a92a397d069a08d7699bf43463076a5209771d', + }, + }; + for (const crypto of cryptos) { + const path = `/v2/blockchain-data/${crypto}/testnet/mempool/fees`; + options.path = path; + res.push(crypto.toUpperCase()); + const result = await safePost(options, ''); + const keys = Object.keys(result.data.item); + keys.shift(); + for (const key of keys) { + res.push(`${key}: ${result.data.item[key]}`); + } + res.push('\n'); + } + console.log(res.join('\n')); + return res.join('\n'); +}; + +const transactionInfo = async (chosenCrypto, hash) => { + const cryptoNames = ['Bitcoin', 'Dash', 'Dogecoin', 'Litecoin']; + const abbreviation = ['btc', 'dash', 'doge', 'ltc']; + + if (chosenCrypto === undefined) { + console.log('\nList of cryptos:'); + for (const value of cryptoNames) { + console.log(`${cryptoNames.indexOf(value) + 1}. ${value}`); + } + + chosenCrypto = await promised.question( + '\nEnter the number' + + ' of crypto from the list above you\'d to like to input hash of:\n' + ); + } + + if (!cryptoNames[chosenCrypto - 1]) { + throw new Error('Error inside function: "Wrong number"'); + } + + if (hash === undefined) { + hash = await promised.question( + '\nEnter the hash of transaction you\'d like to get info about: \n' + ); + } + + const hashDefaultLength = 64; + if (hash.length !== hashDefaultLength) { + throw new Error('Error inside function: "Wrong hash length"'); + } + + const info = await safeGet( + `https://api.blockcypher.com/v1/${ + abbreviation[chosenCrypto - 1] + }/main/txs/${hash}` + ); + const keys = ['total', 'fees', 'size', 'preference', 'received']; + const outputKeys = [ + '\nSatoshis sent', + 'Fee in satoshis', + 'Transaction size in bytes', + 'Transaction preference', + 'Received at', + ]; + + const result = []; + if (Object.prototype.hasOwnProperty.call(info, 'error')) { + throw new Error('Error inside function: "Wrong hash"'); + } else if (Object.prototype.hasOwnProperty.call(info, 'confirmed')) { + keys.push('confirmed'); + outputKeys.push('Confirmed at'); + } else { + result.push('\nTransaction isn\'t confirmed yet :C'); + } + + for (const value of outputKeys) { + result.push(`${value}: ${info[keys[outputKeys.indexOf(value)]]}`); + } + console.log(result.join('\n') + '\n'); + return result.join('\n'); +}; + +module.exports = { + genWalletFeature, + btcAdrBalance, + nbuExchange, + nbuAlternative, + currencyCodeNumber, + monoExchange, + privatExchange, + feesRate, + transactionInfo, +}; diff --git a/src/parser.py b/src/parser.py new file mode 100644 index 0000000..01dc98f --- /dev/null +++ b/src/parser.py @@ -0,0 +1,53 @@ +from html.parser import HTMLParser +from urllib.request import urlopen +import re +import json +import sys + +class CurrencyParser(HTMLParser): + is_tr = False + parsed_data = list() + temp_list = list() + + def __init__(self, site_name, *args, **kwargs): + self.site_name = site_name + super().__init__(*args, **kwargs) + self.feed(self.read_site_content()) + + def read_site_content(self): + return str(urlopen(self.site_name).read()) + + def handle_starttag(self, tag, attrs): + if tag == 'tr': + self.is_tr = True + + def handle_endtag(self, tag): + if tag == 'tr': + self.is_tr = False + find_list = ' '.join(self.temp_list) + currencies = re.findall(r'[A-Z]{3}', find_list) + rate = re.findall(r'[0-9]+,[0-9]+', find_list) + curr_amount = re.findall(r'1[0]{0,3}', find_list) + curr_amount = max(map(int, curr_amount)) + if currencies and rate and curr_amount: + self.parsed_data.append([currencies[0], curr_amount, rate[0] + ' UAH']) + self.temp_list.clear() + + def handle_data(self, data): + if self.is_tr == True: + self.temp_list.append(data) + + def data_to_json(self): + data = list() + for item in self.parsed_data: + data.append( + { + 'currency': item[0], + 'exchangeAmount': item[1], + 'exchangeRate': item[2], + }) + return json.dumps(data) + +parser = CurrencyParser('https://bank.gov.ua/ua/markets/exchangerates') +print(parser.data_to_json()) +sys.stdout.flush() diff --git a/src/promised.js b/src/promised.js new file mode 100644 index 0000000..fa72168 --- /dev/null +++ b/src/promised.js @@ -0,0 +1,132 @@ +'use strict'; + +const fs = require('fs'); +const https = require('https'); +const readline = require('readline'); +const { spawn } = require('child_process'); + +const colors = { + green: '\x1b[32m', + red: '\x1b[31m', + reset: '\x1b[0m', +}; + +const hasOwn = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop); +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}); + +const handler = e => { + //was: console.log instead throw Err + throw new Error(`${e}`); + //process.exit(); +}; + +const classHandler = + (method, Context, ...constr) => + (...args) => { + const ctx = new Context(...constr); + return method.apply(ctx, args).catch(handler); + }; + +const question = str => new Promise(resolve => rl.question(str, resolve)); +const getRequest = async url => + new Promise((resolve, reject) => { + https + .get(url, async res => { + const buffers = []; + for await (const chunk of res) buffers.push(chunk); + const data = JSON.parse(Buffer.concat(buffers).toString()); + if (data.Response === 'Error') reject(data.Message); + resolve(data); + }) + .on('error', reject); + }); + +const postRequest = (options, data) => + new Promise((resolve, reject) => { + const req = https.request(options, async res => { + const buffers = []; + for await (const chunk of res) buffers.push(chunk); + try { + const reqData = JSON.parse(Buffer.concat(buffers).toString()); + resolve(reqData); + } catch (e) { + handler(buffers.toString() + '\n' + e); + } + }); + req.write(data); + req.on('error', reject); + req.end(); + }); + +const promiseSpawn = (lang, path) => + new Promise((resolve, reject) => { + const pyProcess = spawn(lang, [path]); + pyProcess.stdout.on('data', data => resolve(JSON.parse(data))); + pyProcess.stderr.on('data', err => reject(err)); + }); + +const errorWrapper = + handler => + func => + (...args) => + func(...args).catch(handler); + +const classWrapper = (Class, handler, ...args) => { + if (Object.getOwnPropertyNames(Class.prototype).length < 2) { + return Class; + } + const cond = (proto, prop) => + hasOwn(proto, prop) && typeof proto[prop] === 'function'; + for (const prop in Object.getOwnPropertyDescriptors(Class.prototype)) { + if (cond(Class.prototype, prop) && prop !== 'constructor') { + Class.prototype[prop] = handler(Class.prototype[prop], Class, ...args); + } + } + return Class; +}; + +const escapeChars = { lt: '<', gt: '>', quot: '"', apos: '\'', amp: '&' }; + +const regExp = [/&([^;]+);/g, /^#x([\da-fA-F]+)$/, /^#(\d+)$/]; + +function decodeString(str) { + return str.replace(regExp[0], (entity, entityCode) => { + let match; + if (entityCode in escapeChars) { + return escapeChars[entityCode]; + } else if (entityCode.match(regExp[1])) { + match = entityCode.match(regExp[1]); + return String.fromCharCode(parseInt(match[1], 16)); + } else if (entityCode.match(regExp[2])) { + match = entityCode.match(regExp[2]); + return String.fromCharCode(~~match[1]); + } else return entity; + }); +} + +const writeFile = async resultTxt => { + const select = parseInt(await question('Print 1 to save results\n')); + if (select === 1) { + const fileName = 'Write the name of txt file to save your results\n'; + const txtName = await question(fileName); + fs.writeFileSync(`${txtName}.txt`, resultTxt.join('\n'), 'utf8'); + return txtName; + } else return; +}; + +module.exports = { + colors, + question, + writeFile, + getRequest, + postRequest, + promiseSpawn, + errorWrapper, + decodeString, + classWrapper, + handler, + classHandler, +}; diff --git a/src/tests.js b/src/tests.js new file mode 100644 index 0000000..39638cf --- /dev/null +++ b/src/tests.js @@ -0,0 +1,402 @@ +'use strict'; + +const { Crypto } = require('./crypto.js'); +const exchanges = require('./exchanges.js'); +const { colors } = require('./promised.js'); +const assert = require('assert').strict; + +const crypto = new Crypto(); + +(async () => { + // 1 + { + console.log('\n' + + '1. Tests for currencyToCrypto\n' + ); + const expected = /:\s\d/; + // 1-3 work, 4-7 predictable errors + const tests = [ + ['eur', expected, '\'eur\' failed' ], + ['usd', expected, '\'usd\' failed' ], + ['uah', expected, '\'uah\' failed' ], + [5, expected, '5' ], + [null, expected, 'null' ], + ['buu', expected, '\'buu\' failed' ], + ['irl', /Oleksyi/, '\'irl\': Actual !== Expected'], + ]; + + const results = []; + for (const test of tests) { + const [currency, expected, name] = test; + let result; + try { + result = await crypto.currencyToCrypto(currency); + assert.match(result, expected, `Error in test "${name}"`); + } catch (err) { + const { message } = err; + let { operator } = err; + if (!operator) operator = 'insideFunction'; + results.push({ message, currency, operator }); + } + } + console.table(results); + } + // 2 + { + console.log('\n' + + '----------------------------------------------------------------------' + + '\n2. Test for topFiveCurrencies\n' + ); + // Nothing to test except of API correctness + const expected = /^\d.\s\D/; + try { + const result = await crypto.topFiveCurrencies(); + assert.match(result, expected, 'Test failed'); + console.log(colors.green, 'Test passed', colors.reset); + } catch (err) { + console.log(colors.red, err, colors.reset); + } + } + // 3 + { + console.log('\n' + + '----------------------------------------------------------------------' + + '\n3. Tests for currencyPriceVolume\n' + ); + const expected = /is:\s\d/; + // 1-3 work, 4-6 predictable errors + const tests = [ + ['BTC, USD', expected, '\'BTC, USD\' failed' ], + ['ETH, UAH', expected, '\'ETH, UAH\' failed' ], + ['LTC, EUR', expected, '\'DOGE, EUR\' failed'], + [0, expected, '0' ], + ['DOGE, RUB', /Hola/, 'Actual !== Expected' ], + [null, expected, 'null' ], + ]; + + const results = []; + for (const test of tests) { + const [text, expected, name] = test; + let result; + try { + result = await crypto.currencyPriceVolume(text); + assert.match(result, expected, `Error in test "${name}"`); + } catch (err) { + const { message } = err; + let { operator } = err; + if (!operator) operator = 'insideFunction'; + results.push({ message, text, operator }); + } + } + console.table(results); + } + // 5 + { + console.log('\n' + + '----------------------------------------------------------------------' + + '\n5. Test for genWalletFeature\n' + ); + const expected = /private:\w/; + // 1-3 work, 4-6 predictable errors + const tests = [ + [1, expected, '\'1\' failed' ], + [2, expected, '\'2\' failed' ], + [3, expected, '\'3\' failed' ], + [2, /moooo/, 'Actual !== Expected'], + [5, expected, '\'5\' failed' ], + [null, expected, '\'null\' failed' ], + ]; + + const results = []; + for (const test of tests) { + const [selection, expected, name] = test; + let result; + try { + result = await exchanges.genWalletFeature(selection); + assert.match(result, expected, `Error in test "${name}"`); + } catch (err) { + const { message } = err; + let { operator } = err; + if (!operator) operator = 'insideFunction'; + results.push({ message, selection, operator }); + } + } + console.table(results); + } + // 6 + { + console.log('\n' + + '----------------------------------------------------------------------' + + '\n6. Tests for btcAdrBalance\n' + ); + const adr1 = 'bc1qgdjqv0av3q56jvd82tkdjp' + + 'y7gdp9ut8tlqmgrpmv24sq90ecnvqqjwvw97'; + const adr2 = '34xp4vRoCGJym3xR7yCVPFHoCNxv4Twseo'; + const adr3 = '35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP'; + const adr1Expected = 'Total received: 91503610546763 satoshis\n' + + 'Total send: 74852511963191 satoshis\n' + + 'Balance: 16651098583572 satoshis'; + const adr2Expected = 'Total received: 108789803024180 satoshis\n' + + 'Total send: 79447097357947 satoshis\n' + + 'Balance: 29342705666233 satoshis'; + const adr3Expected = 'Total received: 24789994776388 satoshis\n' + + 'Total send: 12844829067961 satoshis\n' + + 'Balance: 11945165708427 satoshis'; + // 1-3 work, 4-6 predictable errors + const tests = [ + [adr1, adr1Expected, 'adr1 failed' ], + [adr2, adr2Expected, 'adr2 failed' ], + [adr3, adr3Expected, 'adr3 failed' ], + ['ss', adr1Expected, 'ss' ], + [123, adr2Expected, '123' ], + [null, 0, 'null, 0' ], + ]; + + const results = []; + for (const test of tests) { + const [adr, expected, name] = test; + let result; + try { + result = await exchanges.btcAdrBalance(adr); + assert.strictEqual(result, expected, `Error in test "${name}"`); + } catch (err) { + const { message } = err; + let { operator } = err; + if (!operator) operator = 'insideFunction'; + results.push({ message, adr, operator }); + } + } + console.table(results); + } + // 7 + { + console.log('\n' + + '----------------------------------------------------------------------' + + '\n7. Test for monoExchange\n' + ); + const expected = 'USD'; + try { + const result = await exchanges.monoExchange(); + assert.strictEqual(result, expected, 'Test failed'); + console.log(colors.green, '\nTest passed', colors.reset); + } catch (err) { + console.log(colors.red, err, colors.reset); + } + } + // 8 + { + console.log('\n' + + '----------------------------------------------------------------------' + + '\n8. Tests for cryptoNews\n' + ); + const expected = /[^1-5.\sa-z]/; + //1-5 & 9 work, others - predictable errors + const tests = [ + [2, expected, 'Choice 2 failed' ], + [1, expected, 'Choice 1 failed' ], + [3, expected, 'Choice 3 failed' ], + [4, expected, 'Choice 4 failed' ], + [5, expected, 'Choice 5 failed' ], + [0, expected, 'Wrong number: 0' ], + [1.5, expected, 'Wrong number: 1.5' ], + [-6, expected, 'Wrong number: -6' ], + ['2', expected, '2 in string' ], + [null, expected, 'null' ], + [Number.NaN, expected, 'Number.Nan' ], + [2, /Hello/, 'Actual(2) !== Expected'], + ]; + + const results = []; + for (const test of tests) { + const [number, expected, name] = test; + let result; + try { + result = await crypto.cryptoNews(number); + assert.match(result, expected, `Error in test "${name}"`); + } catch (err) { + const { message } = err; + let { operator } = err; + if (!operator) operator = 'insideFunction'; + results.push({ message, number, operator }); + } + } + console.table(results); + } + // 9 + { + console.log('\n' + + '----------------------------------------------------------------------' + + '\n9. Tests for privatExchange\n' + ); + const expected = /^\d/; + // 1-4 work, 5-7 predictable errors + const tests = [ + [1, expected, '1 failed' ], + [2, expected, '2 failed' ], + ['1', expected, '\'1\' failed' ], + ['2', expected, '\'2\' failed' ], + [3, expected, 'number 3' ], + [null, expected, 'null' ], + [2, /^\D/, 'Actual !== Expected'], + ]; + + const results = []; + for (const test of tests) { + const [userChoice, expected, name] = test; + let result; + try { + result = await exchanges.privatExchange(userChoice); + assert.match(result, expected, `Error in test "${name}"`); + } catch (err) { + const { message } = err; + let { operator } = err; + if (!operator) operator = 'insideFunction'; + results.push({ message, userChoice, operator }); + } + } + console.table(results); + } + // 10 + { + console.log('\n' + + '----------------------------------------------------------------------' + + '\n10. Test for feesRate\n' + ); + // Nothing to test except of API correctness + const expected = /fast:\s\d/; + try { + const result = await exchanges.feesRate(); + assert.match(result, expected, 'Test failed'); + console.log(colors.green, 'Test passed', colors.reset); + } catch (err) { + console.log(colors.red, err, colors.reset); + } + } + // 11 + { + console.log('\n' + + '----------------------------------------------------------------------' + + '\n11. Test for currencyCodeNumber\n' + ); + // All the magic numbers and string are taken from codeList.json *_* + // 1-4 work, 5-8 predictable errors + const tests = [ + [8, 'ALL', '8 failed' ], + ['DZD', 12, '\'DZD\' failed' ], + ['AUD', 36, '\'AUD\' failed' ], + ['108', 'BIF', '\'108\' failed' ], + [144, 'UZK', 'Actual !== Expected'], + ['ZPO', 8, '\'ZPO\' failed' ], + [null, null, 'null failed' ], + [Number.NaN, 15, 'NaN failed' ], + ]; + + const results = []; + for (const test of tests) { + const [request, expected, name] = test; + let result; + try { + result = await exchanges.currencyCodeNumber(request); + assert.strictEqual(result, expected, `Error in test "${name}"`); + } catch (err) { + const { message } = err; + let { operator } = err; + if (!operator) operator = 'insideFunction'; + results.push({ message, request, operator }); + } + } + console.table(results); + } + // 12 + { + console.log('\n' + + '----------------------------------------------------------------------' + + '\n12. Tests for transactionInfo\n' + ); + + const btcHash = + '8a6f09e9746b7b6f478000835b699565fede84e1485e637e34c69769fcc5968c'; + const dashHash = + '2494f91b633266909084f967d9f0ced7a53064249aea9ca294c4c528e0606b59'; + const dogeHash = + '1c23dd84d4041c187b110714fa8a6827680123036350daa8a40fb1c4cf044825'; + const ltcHash = + '98589bde2f6e39c0ae7c2339bda8363f72080eb6f6269d04c8d17b86bcafa049'; + const btcExpected = '\n' + + 'Satoshis sent: 210936\n' + + 'Fee in satoshis: 12572\n' + + 'Transaction size in bytes: 215\n' + + 'Transaction preference: medium\n' + + 'Received at: 2021-06-02T12:31:59.994Z\n' + + 'Confirmed at: 2021-06-02T12:32:57Z'; + const dashExpected = '\n' + + 'Satoshis sent: 30851258\n' + + 'Fee in satoshis: 25086\n' + + 'Transaction size in bytes: 225\n' + + 'Transaction preference: high\n' + + 'Received at: 2021-06-02T12:34:58.549Z\n' + + 'Confirmed at: 2021-06-02T12:36:04Z'; + const dogeExpected = '\n' + + 'Satoshis sent: 249757626028\n' + + 'Fee in satoshis: 216908020\n' + + 'Transaction size in bytes: 226\n' + + 'Transaction preference: high\n' + + 'Received at: 2021-06-02T12:04:25.207Z\n' + + 'Confirmed at: 2021-06-02T12:05:25Z'; + const ltcExpected = '\n' + + 'Satoshis sent: 67835404\n' + + 'Fee in satoshis: 1660\n' + + 'Transaction size in bytes: 247\n' + + 'Transaction preference: low\n' + + 'Received at: 2021-06-02T12:04:51.983Z\n' + + 'Confirmed at: 2021-06-02T12:12:31Z'; + + //1-4 work, 5-11 predictable errors + const tests = [ + [1, btcHash, btcExpected, 'Bitcion hash' ], + [2, dashHash, dashExpected, 'Dash hash' ], + [3, dogeHash, dogeExpected, 'Doge hash' ], + [4, ltcHash, ltcExpected, 'Litecoin hash' ], + [1, btcHash, dashExpected, 'Dash hash for BTC transaction'], + [3, dogeHash + 'a', dogeExpected, 'Wrong DOGE hash length' ], + [6, btcHash, btcExpected, '6 number' ], + [2, 0, dashExpected, '0 hash' ], + [1, dogeHash, dogeExpected, 'DOGE hash for BTC transaction'], + [4, ltcHash, ltcExpected + '\n', 'Expected !== Actual' ], + [Number.NaN, -2, 0, 'Number.NaN' ], + ]; + + const results = []; + for (const test of tests) { + const [number, hash, expected, name] = test; + let result; + try { + result = await exchanges.transactionInfo(number, hash); + assert.strictEqual(result, expected, `Error in test "${name}"`); + } catch (err) { + const { message } = err; + let { operator } = err; + if (!operator) operator = 'insideFunction'; + results.push({ message, number, hash, operator }); + } + } + console.table(results); + } + // 13 + { + console.log('\n' + + '----------------------------------------------------------------------' + + '\n13. Test for nbuAlternative\n' + ); + // Nothing to test except of API correctness + const expected = /^[A-Z]/; + try { + const result = await exchanges.nbuAlternative(); + assert.match(result, expected, 'Test failed'); + console.log(colors.green, 'Test passed', colors.reset); + } catch (err) { + console.log(colors.red, err, colors.reset); + } + } + process.exit(); +})(); diff --git a/src/themes.md b/src/themes.md new file mode 100644 index 0000000..f5d8df7 --- /dev/null +++ b/src/themes.md @@ -0,0 +1,7 @@ +# Темы, использованные в работе: +1. Async/await +2. Промисификация +3. Работа с массивами, объектами +4. Регулярные выражения +5. Работа с файлами +6. Вывод в консоль diff --git a/src/wallet.js b/src/wallet.js new file mode 100644 index 0000000..772064f --- /dev/null +++ b/src/wallet.js @@ -0,0 +1,60 @@ +'use strict'; + +const promised = require('./promised.js'); + +const errorHandlerWrapped = promised.errorWrapper(promised.handler); + +const safeGet = errorHandlerWrapped(promised.getRequest); + +const safePost = errorHandlerWrapped(promised.postRequest); + +class Wallet { + // _token; + // _keys; + + constructor(currency, token) { + this.defaultUrl = 'api.blockcypher.com'; + this.defaultPath = `/v1/${currency}/main/`; + this._token = token; + } + + async createWallet() { + const path = this.defaultPath + `addrs?token=`; + const data = JSON.stringify({ + token: this._token, + }); + const options = { + method: 'POST', + hostname: this.defaultUrl, + path, + headers: {}, + }; + const result = await safePost(options, data); + this._keys = result; + return result; + } + async getAdrsBalance(adrs) { + const path = `/v1/btc/main/addrs/${adrs}/balance`; + const link = 'https://' + this.defaultUrl + path; + const result = []; + const getInfo = await safeGet(link); + result.push(`Total received: ${getInfo.total_received} satoshis`); + result.push(`Total send: ${getInfo.total_sent} satoshis`); + result.push(`Balance: ${getInfo.balance} satoshis`); + console.log(`${result.join('\n')}\n`); + return result; + } + get keys() { + if (!this._keys) return null; + const walletInfo = []; + const keys = Object.keys(this._keys); + for (const key of keys) { + walletInfo.push(`${key}:${this._keys[key]}\n`); + } + return walletInfo; + } +} + +module.exports = { + Wallet, +};