diff --git a/.gitignore b/.gitignore index b36dafa..09fb8d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,51 @@ -node_modules .DS_Store +.huskyrc.json +out +log.log +**/node_modules +*.pyc +*.vsix +**/.vscode/.ropeproject/** +**/testFiles/**/.cache/** +*.noseids +.nyc_output +.vscode-test +__pycache__ +npm-debug.log +**/.mypy_cache/** +!yarn.lock +coverage/ +cucumber-report.json +**/.vscode-test/** +**/.vscode test/** +**/.vscode-smoke/** +**/.venv*/ +port.txt +precommit.hook +pythonFiles/lib/** +debug_coverage*/** +languageServer/** +languageServer.*/** +bin/** +obj/** +.pytest_cache +tmp/** +.python-version +.vs/ +test-results*.xml +xunit-test-results.xml +build/ci/performance/performance-results.json +!build/ +debug*.log +debugpy*.log +pydevd*.log +nodeLanguageServer/** +nodeLanguageServer.*/** +dist/** +# translation files +*.xlf +package.nls.*.json +l10n/ Thumbs.db -*.log -*.autogenerated -/dist +**/.vscode**/ +build/** \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 6dcfe9c..0000000 --- a/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# GrinPP-CLI - -## Build instructions -* Install Node-js (https://nodejs.org/en/) -* npm install -* npm run release - -GrinNode must be running to use the CLI diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..3743dba --- /dev/null +++ b/build.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +pip install -r requirements.txt +cd src +.venv/bin/python -m nuitka --standalone --onefile --nofollow-import-to=*.tests --nofollow-import-to=*.distutils --nofollow-import-to=tornado.test --assume-yes-for-downloads --output-filename=GrinPP --output-dir="../build" --enable-console --remove-output --warn-unusual-code --include-package=asn1crypto,certifi,cffi,charset_normalizer,click,coincurve,colorama,commonmark,cryptography,idna,psutil,pycparser,pygments,pynostr,requests,rich,shellingham,timeago,tlv8,tornado,typer,urllib3 --include-module=apps,modules --noinclude-pytest-mode=nofollow --noinclude-unittest-mode=nofollow --noinclude-setuptools-mode=nofollow --noinclude-custom-mode=distutils:nofollow --product-name="Grin++ CLI" --product-version="0.2.0" --file-description="Fast, Private and Secure Grin Wallet" src/cli.py \ No newline at end of file diff --git a/build/release.js b/build/release.js deleted file mode 100644 index c2840da..0000000 --- a/build/release.js +++ /dev/null @@ -1,32 +0,0 @@ -const { exec } = require('pkg'); -const jetpack = require('fs-jetpack'); -const os = require('os'); - -const platform = os.platform(); -const package = jetpack.cwd('.').read('package.json', 'json'); -const distFolder = 'dist/'; -const distName = (package.productName).replace(/\s/g, ''); -let distExtension = '.exe'; - -console.log('Packaging release executable for ' + platform); - -if (platform === 'win32') { - //windows - //TODO: remove --public option for production, this is so we can get proper stack trace line #, also depends on import 'source-map-support/register' - exec([ 'package.json', '--target', 'host', '--output', distFolder + distName + distExtension ]).then(() => { - console.log('Packaging done!'); - }).catch((err) => { - console.log('Packaging Error!'); - console.log(err); - }); -} else { - //unix - distExtension = ''; - distName.toLowerCase(); - exec([ 'package.json', '--target', 'host', '--output', distFolder + distName + distExtension ]).then(() => { - console.log('Packaging done!'); - }).catch((err) => { - console.log('Packaging Error!'); - console.log(err); - }); -} diff --git a/cli.js b/cli.js deleted file mode 100644 index 18cd2a4..0000000 --- a/cli.js +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env node -const commander = require('commander'); -const GrinPP = require('./lib/GrinPP.class'); -const commands = require('./lib/cli/commands'); - -async function cli() { - var program = new commander.Command() - .version('0.7.5', '-v, --version', 'output the current version') - .name('GrinPP') - .description('Grin++ CLI'); - - Object.keys(commands.outer).forEach((key) => { - commands.outer[key].add_command(program); - }); - - program.on('--help', function(){ - console.log('') - console.log('Examples:'); - console.log(' $ GrinPP help'); - console.log(' $ GrinPP create username password'); - console.log(' $ GrinPP restore username password word1 word2...word24'); - console.log(' $ GrinPP open username password'); - }); - - await program.parseAsync(process.argv); - - if (global.session_token != null) { - await new GrinPP(global.session_token).execute(); - } -} - -cli(); \ No newline at end of file diff --git a/lib/GrinPP.class.js b/lib/GrinPP.class.js deleted file mode 100644 index 369c227..0000000 --- a/lib/GrinPP.class.js +++ /dev/null @@ -1,45 +0,0 @@ -const commander = require('commander'); - -const commands = require('./cli/commands'); -const readline = require('readline'); - -class GrinPP { - constructor($token) { - this.token = $token; - this.program = new commander.Command() - .usage(' [options]'); - Object.keys(commands.inner).forEach((key) => { - commands.inner[key].add_command(this.program); - }); - } - - async execute() { - this.program._exit = () => {}; - - const args = (await this._waitForInput()).split(' '); - args.unshift('1', ''); - - try { - await this.program.parseAsync(args); - } catch (e) { - console.error("Error:"); - console.error(e); - } - - await this.execute(); - } - - _waitForInput() { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - - return new Promise(resolve => rl.question('> ', ans => { - rl.close(); - resolve(ans); - })); - } -} - -module.exports = GrinPP; \ No newline at end of file diff --git a/lib/api/SendToHTTP.js b/lib/api/SendToHTTP.js deleted file mode 100644 index 1f8367d..0000000 --- a/lib/api/SendToHTTP.js +++ /dev/null @@ -1,79 +0,0 @@ -const WalletAPI = require('./WalletAPI'); - -async function callFinalize(slate) { - const params = { - session_token: global.session_token, - slate: slate, - post_tx: { method: 'STEM' } - }; - - const response = await WalletAPI.OwnerRPC('finalize', params); - if (response != null && response.result != null) { - console.log('Slate finalized:'); - console.log(JSON.stringify(response.result.slate)); - return response.result.slate; - } else { - console.error("Failed to finalize!\n"); - console.error(JSON.stringify(response)); - return null; - } -} - -async function callReceive(httpAddress, slate) { - const response = await WalletAPI.RPC(httpAddress + '/v2/foreign', 'receive_tx', [slate, null, '']); - if (response.result != null && response.result.Ok != null) { - console.log('Slate Received:'); - console.log(JSON.stringify(response.result.Ok)); - return response.result.Ok; - } else { - console.error('Failed while contacting receiver!'); - console.error(JSON.stringify(response)); - return null; - } -} - -async function callSend(httpAddress, amount) { - console.log(global.session_token); - const params = { - session_token: global.session_token, - amount: amount, - fee_base: 1000000, - selection_strategy: { strategy: 'SMALLEST' }, - post_tx: { method: 'STEM' }, - address: httpAddress - }; - - const response = await WalletAPI.OwnerRPC('send', params); - if (response != null && response.result != null) { - console.log('Slate created:'); - console.log(JSON.stringify(response.result.slate)); - return response.result.slate; - } else { - console.error("Failed to send!\n"); - console.error(JSON.stringify(response)); - return null; - } -} - -async function send(httpAddress, amount) { - console.log("Sending to: " + httpAddress); - - const sent_slate = await callSend(httpAddress, amount); - if (sent_slate == null) { - return; - } - - const received_slate = await callReceive(httpAddress, sent_slate); - if (received_slate == null) { - return; - } - - const finalized_slate = await callFinalize(received_slate); - if (finalized_slate == null) { - return; - } - - console.log('Transaction sent successfully!'); -}; - -module.exports = {send} \ No newline at end of file diff --git a/lib/api/WalletAPI.js b/lib/api/WalletAPI.js deleted file mode 100644 index 9da5434..0000000 --- a/lib/api/WalletAPI.js +++ /dev/null @@ -1,66 +0,0 @@ -const axios = require('axios'); - -class WalletAPI { - static async OwnerRPC(method, params) { - const url = 'http://localhost:3421/v2'; - - return await this.RPC(url, method, params); - } - - static async RPC(url, method, params) { - try { - const body = { - id: '0', - jsonrpc: '2.0', - method: method, - params: params - }; - const response = await axios.post(url, body); - return { - error: response.data.error || null, - result: response.data.result || null - }; - } catch (e) { - return { - status_code: e.response.status, - body: e.response.data - }; - } - } - - static async POST(action, headers, body = {}) { - const url = 'http://localhost:3420/v1/wallet/owner/' + action; - - try { - const response = await axios.post(url, body, { headers: headers}); - return { - status_code: response.status, - body: response.data - }; - } catch (e) { - return { - status_code: e.response.status, - body: e.response.data - }; - } - } - - static async GET(action, headers, params = {}) { - const url = 'http://localhost:3420/v1/wallet/owner/' + action; - - try { - const response = await axios.get(url, { params: params, headers: headers}); - return { - status_code: response.status, - body: response.data - }; - } catch (e) { - return { - status_code: e.response.status, - body: e.response.data - }; - } - } -} - -module.exports = WalletAPI; \ No newline at end of file diff --git a/lib/cli/DateFormatter.js b/lib/cli/DateFormatter.js deleted file mode 100644 index 8d1888c..0000000 --- a/lib/cli/DateFormatter.js +++ /dev/null @@ -1,30 +0,0 @@ -function format(date) { - var seconds = Math.floor((new Date() - date) / 1000); - var intervalType; - - var interval = Math.floor(seconds / 86400); - if (interval >= 1) { - return date.toLocaleDateString(); - } else { - interval = Math.floor(seconds / 3600); - if (interval >= 1) { - intervalType = "hour"; - } else { - interval = Math.floor(seconds / 60); - if (interval >= 1) { - intervalType = "minute"; - } else { - interval = seconds; - intervalType = "second"; - } - } - } - - if (interval > 1 || interval === 0) { - intervalType += 's'; - } - - return interval + ' ' + intervalType + ' ago'; -} - -module.exports = { format }; \ No newline at end of file diff --git a/lib/cli/Tables.js b/lib/cli/Tables.js deleted file mode 100644 index b88029a..0000000 --- a/lib/cli/Tables.js +++ /dev/null @@ -1,55 +0,0 @@ -const Table = require('cli-table3'); -const DateFormatter = require('./DateFormatter'); - -function create_table($options = {}) { - $options.chars = { 'top': '═' , 'top-mid': '╤' , 'top-left': '╔' , 'top-right': '╗' - , 'bottom': '═' , 'bottom-mid': '╧' , 'bottom-left': '╚' , 'bottom-right': '╝' - , 'left': '║' , 'left-mid': '╟' , 'mid': '─' , 'mid-mid': '┼' - , 'right': '║' , 'right-mid': '╢' , 'middle': '│' }; - - return new Table($options); -} - -function format_amount(amount) { - var calculatedAmount = Math.abs(amount) / Math.pow(10, 9); - var formatted = calculatedAmount.toFixed(9) + "ツ"; - if (amount < 0) { - formatted = "-" + formatted; - } - - return formatted; -} - -function totals(summary) { - var table = create_table({ head: ['Status', 'Amount']}); - - table.push( - { 'Spendable': [format_amount(summary.spendable)] }, - { 'Total': [format_amount(summary.total)] }, - { 'Immature': [format_amount(summary.immature)] }, - { 'Unconfirmed': [format_amount(summary.unconfirmed)] }, - { 'Locked': [format_amount(summary.locked)] } - ); - - console.log(table.toString()); -} - -function transactions(txs) { - var table = create_table({ head: ["ID", "Created Dt/Tm", "Status", "Address", "Amount"] }); - - txs.forEach((tx, index) => { - table.push( - [ - tx.id, - DateFormatter.format(new Date(tx.creation_date_time * 1000)), - tx.status, - tx.address, - format_amount(tx.amount) - ] - ); - }); - - console.log(table.toString()); -} - -module.exports = { totals, transactions } \ No newline at end of file diff --git a/lib/cli/Wallet.js b/lib/cli/Wallet.js deleted file mode 100644 index 58ae20b..0000000 --- a/lib/cli/Wallet.js +++ /dev/null @@ -1,20 +0,0 @@ -class Wallet { - static set_user(body) { - global.session_token = body.session_token; - global.tor_address = body.tor_address; - global.listener_port = body.listener_port; - - this.display_info(); - } - - static display_info() { - console.log(`Listening on port ${global.listener_port}\n`); - if (global.tor_address != null) { - console.log(`${global.tor_address}`); - console.log(`http://${global.tor_address}.grinplusplus.com`); - console.log(); - } - } -} - -module.exports = Wallet; \ No newline at end of file diff --git a/lib/cli/commands/Clear.js b/lib/cli/commands/Clear.js deleted file mode 100644 index df9f321..0000000 --- a/lib/cli/commands/Clear.js +++ /dev/null @@ -1,17 +0,0 @@ -const WalletAPI = require('../../api/WalletAPI'); -const Tables = require('../Tables'); -const Wallet = require('../Wallet'); - -class Summary { - static add_command(program) { - program.command('clear') - .description('Clears screen') - .action(this.run); - } - - static async run(command_obj) { - console.clear(); - } -} - -module.exports = Summary; \ No newline at end of file diff --git a/lib/cli/commands/CreateWallet.js b/lib/cli/commands/CreateWallet.js deleted file mode 100644 index ced8665..0000000 --- a/lib/cli/commands/CreateWallet.js +++ /dev/null @@ -1,32 +0,0 @@ -const WalletAPI = require('../../api/WalletAPI'); -const Wallet = require('../Wallet'); - -class CreateWallet { - static add_command(program) { - program.command('create ') - .description('Create new wallet') - .action(this.run); - } - - static async run(username, password) { - const headers = { - username: username, - password: password - }; - - const response = await WalletAPI.POST('create_wallet', headers); - if (response != null && response.status_code == 200) { - console.log('Wallet successfully created!\n'); - console.log(response.body.wallet_seed); - console.log(); - - Wallet.set_user(response.body); - } else { - console.error("Failed to create wallet!\n"); - console.error(`Status: ${response.status_code}`); - console.error(`Error: ${response.body}`); - } - } -} - -module.exports = CreateWallet; \ No newline at end of file diff --git a/lib/cli/commands/Exit.js b/lib/cli/commands/Exit.js deleted file mode 100644 index 376ebba..0000000 --- a/lib/cli/commands/Exit.js +++ /dev/null @@ -1,23 +0,0 @@ -const WalletAPI = require('../../api/WalletAPI'); - -class Exit { - static add_command(program) { - program.command('exit') - .description('Exit') - .action(this.run); - } - - static async run() { - if (global.session_token != null) { - const headers = { - session_token: global.session_token - }; - - await WalletAPI.POST('logout', headers); - } - - process.exit(0); - } -} - -module.exports = Exit; \ No newline at end of file diff --git a/lib/cli/commands/Finalize.js b/lib/cli/commands/Finalize.js deleted file mode 100644 index 056e17e..0000000 --- a/lib/cli/commands/Finalize.js +++ /dev/null @@ -1,42 +0,0 @@ -const fs = require('fs'); -const WalletAPI = require('../../api/WalletAPI'); - -var help = false; -var command = null; -class Receive { - static add_command(program) { - command = program.command('finalize ') - .description('Finalize slate file') - .action(this.run) - .allowUnknownOption(true); - command._exit = () => {}; - command.on('--help', () => { help = true; }) - } - - static async run(file) { - if (help) { - help = false; - return; - } - - const slate = fs.readFileSync(file, 'utf-8'); - - const params = { - session_token: global.session_token, - slate: slate, - file: file + '.finalized', - post_tx: { method: 'STEM' } - }; - - const response = await WalletAPI.OwnerRPC('finalize', params); - if (response != null && response.result != null) { - console.log('Finalized successfully'); - console.log(JSON.stringify(response.result.slate)); - } else { - console.error("Failed to finalize!\n"); - console.error(JSON.stringify(response)); - } - } -} - -module.exports = Receive; \ No newline at end of file diff --git a/lib/cli/commands/OpenWallet.js b/lib/cli/commands/OpenWallet.js deleted file mode 100644 index fafceb4..0000000 --- a/lib/cli/commands/OpenWallet.js +++ /dev/null @@ -1,37 +0,0 @@ -const WalletAPI = require('../../api/WalletAPI'); -const Wallet = require('../Wallet'); - -class OpenWallet { - static add_command(program) { - program.command('open ') - .description('Open wallet') - .action(this.run); - } - - static async run(username, password) { - process.stdout.write('Logging in') - const interval = setInterval(() => { process.stdout.write('.'); }, 2500); - - const headers = { - username: username, - password: password - }; - - const response = await WalletAPI.POST('login', headers); - if (response != null && response.status_code == 200) { - clearInterval(interval); - console.clear(); - - Wallet.set_user(response.body); - } else { - clearInterval(interval); - console.clear(); - - console.error("Failed to open wallet!\n"); - console.error(`Status: ${response.status_code}`); - console.error(`Error: ${response.body}`); - } - } -} - -module.exports = OpenWallet; \ No newline at end of file diff --git a/lib/cli/commands/Receive.js b/lib/cli/commands/Receive.js deleted file mode 100644 index cf1bf5d..0000000 --- a/lib/cli/commands/Receive.js +++ /dev/null @@ -1,46 +0,0 @@ -const fs = require('fs'); -const WalletAPI = require('../../api/WalletAPI'); - -var help = false; -var command = null; -class Receive { - static add_command(program) { - command = program.command('receive ') - .description('Receive slate file') - .option('-m, --message ', 'Message') - .action(this.run) - .allowUnknownOption(true); - command._exit = () => {}; - command.on('--help', () => { help = true; }) - } - - static async run(file, command_obj) { - if (help) { - help = false; - return; - } - - const slate = fs.readFileSync(file, 'utf-8'); - - const params = { - session_token: global.session_token, - slate: slate, - file: file + '.response' - }; - - if (command_obj.message != null && command_obj.message.length > 0) { - params.message = command_obj.message; - } - - const response = await WalletAPI.OwnerRPC('receive', params); - if (response != null && response.result != null) { - console.log('Received successfully'); - console.log(JSON.stringify(response.result.slate)); - } else { - console.error("Failed to receive!\n"); - console.error(JSON.stringify(response)); - } - } -} - -module.exports = Receive; \ No newline at end of file diff --git a/lib/cli/commands/RestoreWallet.js b/lib/cli/commands/RestoreWallet.js deleted file mode 100644 index 1105a27..0000000 --- a/lib/cli/commands/RestoreWallet.js +++ /dev/null @@ -1,33 +0,0 @@ -const WalletAPI = require('../../api/WalletAPI'); -const Wallet = require('../Wallet'); - -class RestoreWallet { - static add_command(program) { - program.command('restore ') - .description('Restore wallet from seed') - .action(this.run); - } - - static async run(username, password, seed_words) { - const headers = { - username: username, - password: password - }; - - const body = { - wallet_seed: seed_words.join(' ') - }; - - const response = await WalletAPI.POST('restore_wallet', headers, body); - if (response != null && response.status_code == 200) { - console.log('Wallet successfully restored!\n'); - Wallet.set_user(response.body); - } else { - console.error("Failed to restore wallet!\n"); - console.error(`Status: ${response.status_code}`); - console.error(`Error: ${response.body}`); - } - } -} - -module.exports = RestoreWallet; \ No newline at end of file diff --git a/lib/cli/commands/Send.js b/lib/cli/commands/Send.js deleted file mode 100644 index 7625135..0000000 --- a/lib/cli/commands/Send.js +++ /dev/null @@ -1,61 +0,0 @@ -const WalletAPI = require('../../api/WalletAPI'); -const SendToHTTP = require('../../api/SendToHTTP'); - -var help = false; -var command = null; -class Send { - static add_command(program) { - command = program.command('send ') - .usage('-m -d ') - .description('Send grins (ex. send -m tor -d 3ngg5chiucyvjxaymy46fypqbr3nfskj2lluygnz6hqys5lwxrcpqzad 2.5)') - .requiredOption('-m, --method ', 'Send method (tor, http, file)') - .requiredOption('-d, --destination ', 'Destination file or address') - .action(this.run) - .allowUnknownOption(true); - command._exit = () => {}; - command.on('--help', () => { help = true; }) - } - - static async run(amount, command_obj) { - if (help) { - help = false; - return; - } - - if (command_obj.destination == null) { - console.error('destination required'); - return; - } - - var params = { - session_token: global.session_token, - amount: amount * Math.pow(10, 9), - fee_base: 1000000, - selection_strategy: { strategy: 'SMALLEST' }, - post_tx: { method: 'STEM' } - }; - - const method = command_obj.method == null ? null : command_obj.method.toLowerCase(); - if (method == 'tor') { - params.address = command_obj.destination; - } else if (method == 'file') { - params.file = command_obj.destination; - } else if (method == 'http') { - return await SendToHTTP.send(command_obj.destination, amount * Math.pow(10, 9)); - } else { - console.error('method required (tor, http, file)'); - return; - } - - const response = await WalletAPI.OwnerRPC('send', params); - if (response != null && response.result != null) { - console.log('Sent successfully'); - console.log(JSON.stringify(response.result.slate)); - } else { - console.error("Failed to send!\n"); - console.error(JSON.stringify(response)); - } - } -} - -module.exports = Send; \ No newline at end of file diff --git a/lib/cli/commands/Summary.js b/lib/cli/commands/Summary.js deleted file mode 100644 index ec19b78..0000000 --- a/lib/cli/commands/Summary.js +++ /dev/null @@ -1,79 +0,0 @@ -const WalletAPI = require('../../api/WalletAPI'); -const Tables = require('../Tables'); -const Wallet = require('../Wallet'); - -var help = false; -var command = null; -class Summary { - static add_command(program) { - command = program.command('summary') - .description('Wallet summary') - .option('-c, --canceled', 'include canceled txs') - .action(this.run) - .allowUnknownOption(true); - command._exit = () => {}; - command.on('--help', () => { help = true; }) - } - - static get_status(txn, lastConfirmedHeight) { - const status = txn.type; - if (status == "Sent" || status == "Received") { - if ((txn.confirmed_height + 9) > lastConfirmedHeight) { - return status + " (" + (lastConfirmedHeight - txn.confirmed_height + 1) + " Confirmations)"; - } - - return status; - } else if (status == "Sending (Finalized)") { - return "Sending (Unconfirmed)"; - } else { - return status; - } - } - - static async run(command_obj) { - if (help) { - help = false; - return; - } - - Wallet.display_info(); - - const headers = { - session_token: global.session_token - }; - - const response = await WalletAPI.GET('retrieve_summary_info', headers, {}); - if (response != null && response.status_code == 200) { - const summary = response.body; - Tables.totals({ - spendable: summary.amount_currently_spendable, - total: summary.total, - immature: summary.amount_immature, - unconfirmed: summary.amount_awaiting_confirmation, - locked: summary.amount_locked - }); - - const txs = []; - response.body.transactions.forEach((tx, index) => { - const status = Summary.get_status(tx, response.body.last_confirmed_height); - if (command_obj.canceled || status != 'Canceled') { - txs.push({ - id: tx.id, - amount: tx.amount_credited - tx.amount_debited, - address: tx.address, - status: status, - creation_date_time: tx.creation_date_time - }); - } - }); - - Tables.transactions(txs); - } else { - console.error("Failed to retrieve wallet summary!\n"); - console.error(`Status: ${response.status_code}`); - console.error(`Error: ${response.body}`); - } - } -} - -module.exports = Summary; \ No newline at end of file diff --git a/lib/cli/commands/index.js b/lib/cli/commands/index.js deleted file mode 100644 index 7fea844..0000000 --- a/lib/cli/commands/index.js +++ /dev/null @@ -1,15 +0,0 @@ -const outer = {}; -outer.CreateWallet = require('./CreateWallet'); -outer.RestoreWallet = require('./RestoreWallet'); -outer.OpenWallet = require('./OpenWallet'); - -const inner = {}; -inner.Summary = require('./Summary'); -inner.Send = require('./Send'); -inner.Receive = require('./Receive'); -inner.Finalize = require('./Finalize'); -inner.Clear = require('./Clear'); -inner.Exit = require('./Exit'); - -exports.outer = outer; -exports.inner = inner; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 6840fd6..0000000 --- a/package-lock.json +++ /dev/null @@ -1,2523 +0,0 @@ -{ - "name": "grinpp", - "version": "1.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@babel/parser": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.4.5.tgz", - "integrity": "sha512-9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew==", - "dev": true - }, - "@babel/runtime": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.5.tgz", - "integrity": "sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.13.2" - } - }, - "@mrmlnc/readdir-enhanced": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", - "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", - "dev": true, - "requires": { - "call-me-maybe": "^1.0.1", - "glob-to-regexp": "^0.3.0" - } - }, - "@nodelib/fs.stat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", - "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", - "dev": true - }, - "@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" - }, - "@types/events": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", - "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", - "dev": true - }, - "@types/glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", - "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", - "dev": true, - "requires": { - "@types/events": "*", - "@types/minimatch": "*", - "@types/node": "*" - } - }, - "@types/minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", - "dev": true - }, - "@types/node": { - "version": "12.7.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.5.tgz", - "integrity": "sha512-9fq4jZVhPNW8r+UYKnxF1e2HkDWOWKM5bC2/7c9wPV835I0aOrVbS/Hw/pWPk2uKrNXQqg9Z959Kz+IYDd5p3w==", - "dev": true - }, - "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", - "dev": true, - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "dev": true, - "requires": { - "array-uniq": "^1.0.1" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", - "dev": true - }, - "axios": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", - "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", - "requires": { - "follow-redirects": "1.5.10" - } - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "byline": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", - "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=", - "dev": true - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, - "call-me-maybe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", - "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", - "dev": true - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "cli-table3": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz", - "integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==", - "requires": { - "colors": "^1.1.2", - "object-assign": "^4.1.0", - "string-width": "^2.1.1" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "optional": true - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==" - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "dir-glob": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", - "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", - "dev": true, - "requires": { - "path-type": "^3.0.0" - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dev": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "escodegen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.1.tgz", - "integrity": "sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw==", - "dev": true, - "requires": { - "esprima": "^3.1.3", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - } - }, - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, - "fast-glob": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", - "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==", - "dev": true, - "requires": { - "@mrmlnc/readdir-enhanced": "^2.2.1", - "@nodelib/fs.stat": "^1.1.2", - "glob-parent": "^3.1.0", - "is-glob": "^4.0.0", - "merge2": "^1.2.3", - "micromatch": "^3.1.10" - } - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "follow-redirects": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", - "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", - "requires": { - "debug": "=3.1.0" - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "requires": { - "map-cache": "^0.2.2" - } - }, - "from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "fs-jetpack": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/fs-jetpack/-/fs-jetpack-2.2.2.tgz", - "integrity": "sha512-USJrUxck7SIXSvYPzU5fuR5iqLHRDSzb0kHvCJlQhUGEVai3P9yZDu/2b+bAzprbWLCc2YcslxBLBUInDmYkYA==", - "dev": true, - "requires": { - "minimatch": "^3.0.2", - "rimraf": "^2.6.3" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "glob-to-regexp": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", - "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=", - "dev": true - }, - "globby": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-9.2.0.tgz", - "integrity": "sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==", - "dev": true, - "requires": { - "@types/glob": "^7.1.1", - "array-union": "^1.0.2", - "dir-glob": "^2.2.2", - "fast-glob": "^2.2.6", - "glob": "^7.1.3", - "ignore": "^4.0.3", - "pify": "^4.0.1", - "slash": "^2.0.0" - } - }, - "graceful-fs": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", - "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==", - "dev": true - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "dev": true, - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "into-stream": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-5.1.0.tgz", - "integrity": "sha512-cbDhb8qlxKMxPBk/QxTtYg1DQ4CwXmadu7quG3B7nrJsgSncEreF2kwWKZFdnjc/lSNNIkFPsjI7SM0Cx/QXPw==", - "dev": true, - "requires": { - "from2": "^2.3.0", - "p-is-promise": "^2.0.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "requires": { - "p-locate": "^4.1.0" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, - "merge2": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz", - "integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", - "dev": true - }, - "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", - "dev": true, - "requires": { - "mime-db": "1.40.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - } - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "multistream": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/multistream/-/multistream-2.1.1.tgz", - "integrity": "sha512-xasv76hl6nr1dEy3lPvy7Ej7K/Lx3O/FCvwge8PeVJpciPPoNCbaANcNiBug3IpdvTveZUcAV0DJzdnUDMesNQ==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.5" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - } - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "^3.0.0" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true - }, - "p-limit": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", - "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } - } - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, - "pkg": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/pkg/-/pkg-4.4.0.tgz", - "integrity": "sha512-bFNJ3v56QwqB6JtAl/YrczlmEKBPBVJ3n5nW905kgvG1ex9DajODpTs0kLAFxyLwoubDQux/RPJFL6WrnD/vpg==", - "dev": true, - "requires": { - "@babel/parser": "~7.4.4", - "@babel/runtime": "~7.4.4", - "chalk": "~2.4.2", - "escodegen": "~1.11.1", - "fs-extra": "~7.0.1", - "globby": "~9.2.0", - "into-stream": "~5.1.0", - "minimist": "~1.2.0", - "multistream": "~2.1.1", - "pkg-fetch": "~2.6.2", - "progress": "~2.0.3", - "resolve": "1.6.0", - "stream-meter": "~1.0.4" - } - }, - "pkg-fetch": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-2.6.2.tgz", - "integrity": "sha512-7DN6YYP1Kct02mSkhfblK0HkunJ7BJjGBkSkFdIW/QKIovtAMaICidS7feX+mHfnZ98OP7xFJvBluVURlrHJxA==", - "dev": true, - "requires": { - "@babel/runtime": "~7.4.4", - "byline": "~5.0.0", - "chalk": "~2.4.1", - "expand-template": "~2.0.3", - "fs-extra": "~7.0.1", - "minimist": "~1.2.0", - "progress": "~2.0.0", - "request": "~2.88.0", - "request-progress": "~3.0.0", - "semver": "~6.0.0", - "unique-temp-dir": "~1.0.0" - }, - "dependencies": { - "semver": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.0.0.tgz", - "integrity": "sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ==", - "dev": true - } - } - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true - }, - "psl": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.4.0.tgz", - "integrity": "sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw==", - "dev": true - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "regenerator-runtime": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", - "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==", - "dev": true - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - } - } - }, - "request-progress": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", - "integrity": "sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=", - "dev": true, - "requires": { - "throttleit": "^1.0.0" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, - "resolve": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.6.0.tgz", - "integrity": "sha512-mw7JQNu5ExIkcw4LPih0owX/TZXjD/ZUF/ZQ/pDnkw3ZKhDcZZw5klmBlj6gVMwjQ3Pz5Jgu7F3d0jcDVuEWdw==", - "dev": true, - "requires": { - "path-parse": "^1.0.5" - } - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", - "dev": true - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true - }, - "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "dev": true, - "requires": { - "atob": "^2.1.1", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "dev": true - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "stream-meter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/stream-meter/-/stream-meter-1.0.4.tgz", - "integrity": "sha1-Uq+Vql6nYKJJFxZwTb/5D3Ov3R0=", - "dev": true, - "requires": { - "readable-stream": "^2.1.4" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "throttleit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", - "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=", - "dev": true - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "dev": true, - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - } - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "uid2": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", - "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=", - "dev": true - }, - "union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - } - }, - "unique-temp-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unique-temp-dir/-/unique-temp-dir-1.0.0.tgz", - "integrity": "sha1-bc6VsmgcoAPuv7MEpBX5y6vMU4U=", - "dev": true, - "requires": { - "mkdirp": "^0.5.1", - "os-tmpdir": "^1.0.1", - "uid2": "0.0.3" - } - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true - } - } - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "uuid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", - "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==", - "dev": true - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" - }, - "yargs": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.1.0.tgz", - "integrity": "sha512-T39FNN1b6hCW4SOIk1XyTOWxtXdcen0t+XYrysQmChzSipvhBO8Bj0nK1ozAasdk24dNWuMZvr4k24nz+8HHLg==", - "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^16.1.0" - } - }, - "yargs-parser": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-16.1.0.tgz", - "integrity": "sha512-H/V41UNZQPkUMIT5h5hiwg4QKIY1RPvoBV4XcjUbRM8Bk2oKqqyZ0DIEbTFZB0XjbtSPG8SAa/0DxCQmiRgzKg==", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 5196ee7..0000000 --- a/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "grinpp", - "productName": "grinpp", - "version": "1.0.0", - "description": "CLI for Grin++", - "main": "cli.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "release": "node build/release.js", - "start": "node cli.js" - }, - "author": "David Burkett", - "license": "MIT", - "bin": { - "grinpp": "./cli.js" - }, - "dependencies": { - "axios": "^0.19.2", - "cli-table3": "^0.5.1", - "commander": "^4.1.1", - "yargs": "^15.1.0" - }, - "devDependencies": { - "fs-jetpack": "^2.2.2", - "pkg": "^4.4.0" - } -} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3ebb4ae --- /dev/null +++ b/requirements.txt @@ -0,0 +1,25 @@ +asn1crypto==1.5.1 +certifi==2023.7.22 +cffi==1.16.0 +charset-normalizer==3.3.0 +click==8.1.7 +coincurve==18.0.0 +colorama==0.4.6 +commonmark==0.9.1 +cryptography==41.0.4 +idna==3.4 +markdown-it-py==3.0.0 +mdurl==0.1.2 +psutil==5.9.5 +pycparser==2.21 +Pygments==2.16.1 +pynostr==0.6.2 +requests==2.31.0 +rich==13.6.0 +shellingham==1.5.3 +timeago==1.0.16 +tlv8==0.10.0 +tornado==6.3.3 +typer==0.9.0 +typing_extensions==4.8.0 +urllib3==2.0.5 diff --git a/src/apps/__init__.py b/src/apps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/apps/misc/__init__.py b/src/apps/misc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/apps/misc/app.py b/src/apps/misc/app.py new file mode 100644 index 0000000..76caba6 --- /dev/null +++ b/src/apps/misc/app.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 + +from typing import Optional + +import psutil +import typer +from rich import box +from rich.console import Console +from rich.table import Table + +from modules.utils import grinchck, processes +from modules.utils.coingecko import get_grin_price + +app = typer.Typer() + +console = Console(width=125, style="grey93") +error_console = Console(stderr=True, style="bright_red", width=125) + + +@app.command(name="wenmoon") +def simple_grin_price( + currency: str = typer.Option( + "usd", help="Currency like usd, eur, btc, etc.", prompt=True + ) +): + """ + Get Grin price from CoinGecko. + """ + try: + moon = get_grin_price(currency) + except Exception as err: + error_console.print(f"Error: {err}") + raise typer.Abort() + table = Table(box=box.HORIZONTALS, show_header=False) + direction: str = "" + style: str = "" + if moon["24h_change"] >= 0: + style = "green3" + direction = "⬆︎" + else: + style = "bright_red" + direction = "⬇︎" + change: str = f"[{style}]{moon['24h_change']:10,.2f}% {direction}" + + table.add_column("", justify="right", style="bold") + table.add_column("", justify="right") + table.add_row("Price:", f"{moon['price']:10,.6f}") + table.add_row("24h Change:", f"{change}") + table.add_row("Market cap:", f"{moon['market_cap']:10,.6f}") + table.add_row("24h Volume:", f"{moon['24h_vol']:10,.6f}") + + console.print(table) + + +@app.command(name="grinchck") +def grinchck_test( + address: str = typer.Option( + ..., help="Address of the wallet you want to check", prompt="Wallet address" + ) +): + """ + Check if a Slatepack Addresss is reachable via the Tor network. + """ + reachable: bool = False + try: + with console.status("Checking wallet reachability..."): + reachable = grinchck.connect( + slatepack_address=address, api_url="http://192.227.214.130/" + ) + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + if reachable: + console.print( + f"Address [green3]{address}[/green3] is reachable via the Tor Network <(^_^)>" + ) + else: + error_console.print( + f"Addres [dark_orange]{address}[/dark_orange] is not reachable the Tor Network \_(-_-)_/" + ) + raise typer.Exit() + + +@app.command(name="tor") +def tor_control( + stop: Optional[bool] = typer.Option( + False, + help="Stop the tor process", + rich_help_panel="Actions", + ) +): + """ + Check the status of Tor. + + If --stop is used, tor will be stopped + """ + running: bool = False + tor_process_name: str = "tor" + if psutil.WINDOWS: + tor_process_name = "tor.exe" + tor_processes: list[psutil.Process] = processes.find(tor_process_name) + + if len(tor_processes) > 0: + running = True + + if not running: + error_console.print("Tor is not running") + raise typer.Abort() + elif running: + for process in tor_processes: + console.print(f"Tor is running (PID: [bold]{process.pid})") + if stop: + with console.status("Stopping tor..."): + for process in tor_processes: + processes.kill(process.pid) + if len(processes.find(tor_process_name)) > 0: + error_console.print("Tor is still running") + else: + console.print("Tor [bold]stopped[/bold] successfully ✔") + + raise typer.Exit() diff --git a/src/apps/node/__init__.py b/src/apps/node/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/apps/node/app.py b/src/apps/node/app.py new file mode 100644 index 0000000..937a63b --- /dev/null +++ b/src/apps/node/app.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 + +import json +import subprocess +import time +from pathlib import Path + +import psutil +import typer +from rich import box +from rich.console import Console +from rich.progress import Progress +from rich.table import Table +from requests import exceptions + +from modules.api.foreign.rpc import get_list_of_settings +from modules.api.node.rest import ( + get_node_connected_peers, + get_node_state, + get_sync_state, + shutdown_node, +) +from modules.api.owner.v2 import node +from modules.utils import processes + +app = typer.Typer() + +console = Console(width=125, style="grey93") +error_console = Console(stderr=True, style="bright_red", width=125) + + +@app.command(name="stop") +def stop_node(): + """ + Close all running Wallets and stop the running Tor Listeners. + """ + running: bool = False + node_process_name: str = "GrinNode" + if psutil.WINDOWS: + node_process_name = "GrinNode.exe" + if len(processes.find(node_process_name)) > 0: + running = True + + if not running: + error_console.print("Grin node is not running") + raise typer.Abort() + + elif running: + with console.status("Stopping node..."): + attempt = 0 + while attempt < 10: + time.sleep(1) + attempt += 1 + try: + shutdown_node() + break + except: + pass + if len(processes.find(node_process_name)) == 0: + console.print("Node and wallet Tor Listeners successfully [bold]stopped[/bold]") + else: + error_console.print("Unable to stop Node.") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="start") +def start_node(): + """ + Launch the Grin node in the background. + """ + data = json.loads("{}") + error = None + with console.status("Starting node..."): + if not Path(f"{Path(__file__).parent.resolve()}/../bin/GrinNode.exe").is_file(): + error = "Can't find the Node" + raise typer.Exit() + + if psutil.WINDOWS: + subprocess.Popen( + f"{Path('../bin/GrinNode.exe').absolute()} --headless", + shell=True, + start_new_session=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.STDOUT, + ) + + while data is None: + try: + data = get_node_state() + except Exception: + pass + if error: + error_console.print(error) + raise typer.Abort() + + console.print("Node successfully [bold]launched[/bold] ✔") + + raise typer.Exit() + + +@app.command(name="clean") +def clean(): + """ + Delete the local chain data and let the node sync again from scratch. + """ + with console.status("Stopping node..."): + while True: + try: + shutdown_node() + break + except Exception: + pass + + console.print("Node [bold]stopped[/bold] successfully ✔") + + +@app.command(name="status") +def get_node_status(): + """ + Get the current status of the running node. + """ + + connerr: exceptions.ConnectionError + error = "" + + message = "Getting Node Status..." + percentage = 0.0 + + console.print("Ctrl+C to quit...\n", style="grey42 italic", justify="right") + with Progress(console=console) as progress: + task = progress.add_task( + f"[bold white]{message}", total=100, completed=int(percentage) + ) + while True: + progress.update( + task, description=f"[bold white]{message}", completed=percentage + ) + time.sleep(1) + try: + node_status = node.get_status() + message, percentage = node.parse_node_status(node_status) + except exceptions.ConnectionError as err: + error = "Unable to communicate with the API" + break + except Exception as e: + error = str(e) + break + if error: + error_console.print(error) + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="tip") +def get_node_tip(): + """ + Print the heights of the node, this is important to see if the running Node is synchronized or not. + """ + + try: + data = node.get_status() + table = Table(box=box.HORIZONTALS, expand=True) + table.add_column("node height", justify="center") + table.add_column("network height", justify="center") + table.add_column("chain height", justify="center") + table.add_column("current hash", justify="center") + + table.add_row( + str(data["sync_info"]["current_height"]), + str(data["sync_info"]["highest_height"]), + str(data["tip"]["height"]), + data["tip"]["last_block_pushed"], + ) + + console.print(table) + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="settings") +def get_node_settings(): + """ + Obtain the node's settings like amount of confirmations, minimum of outbound connections, etc. + """ + data = json.loads("{}") + + try: + data = get_list_of_settings() + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + table = Table(box=box.HORIZONTALS, expand=True) + table.add_column("setting", justify="right") + table.add_column("value", justify="full", style="bold") + + table.add_row("confirmations required:", str(data["result"]["min_confirmations"])) + table.add_row("maximum of outbounds:", str(data["result"]["max_peers"])) + table.add_row( + "maximum of inbounds:", + str(data["result"]["max_peers"] - data["result"]["min_peers"]), + ) + table.add_row( + "peers preferred:", + f'[{", ".join(sorted(data["result"]["preferred_peers"]))}]' + if data["result"]["preferred_peers"] != None + else "[ ]", + ) + table.add_row( + "peers allowed:", + ", ".join(sorted(data["result"]["allowed_peers"])) + if data["result"]["allowed_peers"] != None + else "[ ]", + ) + table.add_row( + "peers blocked:", + ", ".join(sorted(data["result"]["blocked_peers"])) + if data["result"]["blocked_peers"] != None + else "[ ]", + ) + + console.print(table) + + raise typer.Exit() + + +@app.command(name="peers") +def list_connected_peers(): + """ + List the inbound and outbound peers connected to the running node. + """ + + try: + data = node.get_connected_peers() + + table = Table(box=box.HORIZONTALS, expand=True) + table.add_column("", justify="center", width=5) + table.add_column("address", justify="center", width=20) + table.add_column("agent", justify="center", width=20) + table.add_column("direction", justify="center", width=20) + + i = 1 + for peer in data: + table.add_row(str(i), peer["addr"], peer["user_agent"], peer["direction"]) + i += 1 + + console.print(table) + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() diff --git a/src/apps/transaction/__init__.py b/src/apps/transaction/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/apps/transaction/app.py b/src/apps/transaction/app.py new file mode 100644 index 0000000..23c17de --- /dev/null +++ b/src/apps/transaction/app.py @@ -0,0 +1,520 @@ +#!/usr/bin/env python3 + +from datetime import datetime +from typing import Optional +from pathlib import Path +import base64 + +import json +import psutil +import timeago +import typer +from rich import box +from rich.console import Console +from rich.prompt import Confirm +from rich.table import Table + +from apps.transaction.nostr import transport +from modules.api import TransactionsFilterOptions +from modules.api.owner.rpc import ( + add_signature_to_transaction, +) +from modules.api.owner.v3.wallet import ( + cancel_tx, + decode_slatepack, + finalize_tx, + get_stored_tx, + get_tx_details, + post_tx, + receive_tx, + retrieve_txs, + estimate_fee, + send_tx, +) +from modules.wallet import session + +if not psutil.WINDOWS: + import readline + +app = typer.Typer() + +app.add_typer( + transport.plugin, + name="nostr", + no_args_is_help=True, + help="Nostr Transport Plugin for Grin transactions.", +) +console = Console(width=155, style="grey93") +error_console = Console(stderr=True, style="bright_red", width=155) + + +@app.command(name="list") +def list_transactions( + wallet: str = typer.Option( + ..., + help="Name of the wallet from which you want to list transactions.", + prompt="Wallet name", + ), + password: str = typer.Option( + ..., help="Wallet password.", prompt="Password", hide_input=True + ), + status: TransactionsFilterOptions = typer.Option( + default=TransactionsFilterOptions.pending.value, + help="Status of the transactions you want to list.", + ), +): + """ + List the transactions of a running Wallet. Default status: Pending. + """ + + try: + session_token = session.token(wallet=wallet, password=password) + + transactions = retrieve_txs(session_token, status.value) + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + if len(transactions) == 0: + console.print(f"No [bold]{status.value}[/bold] transaction were found.") + raise typer.Exit() + + table = Table(title="", box=box.HORIZONTALS, expand=True) + table.add_column("tx_id", justify="center", width=5) + table.add_column("slate_id", justify="center", width=40) + table.add_column("amount ツ", justify="right", width=15) + table.add_column("fees ツ", justify="right", width=15) + table.add_column("when?", justify="center", width=15) + table.add_column("status", justify="center", width=10) + table.add_column("height", justify="center") + table.add_column("explorer", justify="center") + + style: str = "" + status: str = "" + for transaction in transactions: + style = "" + status = transaction["type"].lower() + + if status == "sent": + style = "deep_sky_blue1" + elif status == "received": + style = "spring_green3" + elif status == "canceled": + style = "grey66" + elif status == "sending (not finalized)": + style = "royal_blue1" + status = "unfinalized" + elif status == "sending (finalized)": + style = "cornflower_blue" + status = "unconfirmed" + elif status == "receiving (unconfirmed)": + style = "cyan3" + status = "unconfirmed" + + amount: float = 0 + fee = 0 + if "fee" in transaction: + fee = transaction["fee"] + + if transaction["amount_debited"] - transaction["amount_credited"] > 0: + amount = transaction["amount_credited"] - transaction["amount_debited"] + if "fee" in transaction: + amount = amount - fee + else: + amount = transaction["amount_debited"] - transaction["amount_credited"] + amount = abs(amount) + amount = amount / pow(10, 9) + fee = fee / pow(10, 9) + + relative_date = timeago.format( + datetime.fromtimestamp(transaction["creation_date_time"]) + ) + link: str = "" + confirmed_height = "-" + if "confirmed_height" in transaction: + confirmed_height = transaction["confirmed_height"] + link = f"[link=https://grinexplorer.net/output/{transaction['outputs'][0]['commitment']}]View in Explorer[/link]" + + table.add_row( + f"{transaction['id']}", + f"{transaction.get('slate_id', '')}", + f"{amount:10,.9f}", + f"{fee:10,.9f}" if fee > 0 else "-", + f"{relative_date}", + status, + f"{confirmed_height}", + link, + style=style, + ) + console.print(table) + + raise typer.Exit() + + +@app.command(name="send") +def send( + wallet: str = typer.Option( + ..., + help="Name of the wallet from which you wish to send the coins.", + prompt="Wallet name", + ), + password: str = typer.Option( + ..., help="Wallet password.", prompt="Password", hide_input=True + ), + amount: Optional[float] = typer.Option(None, help="Amount of ツ you want to send."), + address: Optional[str] = typer.Option( + "", help="Slatepack Address where you want to send the ツ" + ), + auto: Optional[bool] = typer.Option( + False, help="Auto answer 'Yes' to all confirmations" + ), +): + """ + Send ツ to someone + """ + + fee: float + + try: + token = session.token(wallet=wallet, password=password) + if not amount: + estimate = estimate_fee(session_token=token) + fee = float(estimate["fee"]) / pow(10, 9) + amount_prt = float(estimate["amount"]) / pow(10, 9) + else: + estimate = estimate_fee(session_token=token, amount=amount) + fee = float(estimate["fee"]) / pow(10, 9) + amount_prt = float(estimate["amount"]) / pow(10, 9) + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + table: Table = Table( + title="Transaction details", box=box.HORIZONTALS, expand=True, show_header=False + ) + table.add_column("", justify="right", style="bold") + table.add_column("", justify="left") + table.add_row("wallet:", f"{wallet}") + table.add_row("amount:", f"{amount_prt:10,.9f} ツ") + table.add_row("fee:", f"{fee:10.9f} ツ") + if address: + table.add_row("receiver:", f"{address}") + + console.print(table) + console.print("") + proceed: bool = False + if auto: + proceed = True + else: + proceed = Confirm.ask( + "Are you sure you want to send this transaction?", default=False + ) + if proceed: + sent: bool = False + slatepack: str = "" + try: + session_token = session.token(wallet=wallet, password=password) + with console.status("Building transaction..."): + if not amount: + transaction = send_tx(session_token=session_token, address=address) + else: + transaction = send_tx( + session_token=session_token, + amount=amount, + address=address, + ) + slatepack = transaction["slatepack"] + if transaction["status"] == "FINALIZED": + sent = True + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + if sent: + console.print(f"Transaction built with {address} via Tor successfully ✔") + elif not sent: + error_console.print(f"Unable to build transaction with {address} via Tor ✗") + + console.print("\nPlease share the next Slatepack with the recipient:") + console.print( + f"\n[yellow1]{slatepack.strip().rstrip()}[yellow1]\n", + ) + console.print( + "*** Ask the recipient to add his signature by receiving this slatepack.", + style="italic", + ) + console.print( + "Then execute the [bold]finalize[/bold] command and insert the signed Slatepack by him/she. ***", + style="italic", + ) + + raise typer.Exit() + + +@app.command(name="cancel") +def cancel( + wallet: str = typer.Option( + ..., help="Name of the wallet from which you wish to cancel the transaction." + ), + password: str = typer.Option( + ..., help="Wallet password.", prompt="Password", hide_input=True + ), + tx_id: int = typer.Option( + ..., + help="Id of the transaction you want to be canceled.", + prompt="Transaction Id", + ), +): + """ + Cancel a transaction using the Transaction Id. + """ + + try: + token = session.token(wallet=wallet, password=password) + + if cancel_tx(session_token=token, tx_id=tx_id): + console.print("Transaction [bold]canceled[/bold] successfully ✔") + else: + error_console.print("Unable to cancel the transaction ✗") + + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="finalize") +def finalize( + wallet: str = typer.Option( + ..., + help="Name of the wallet from which you wish to finalize the transaction.", + prompt="Wallet name", + ), + password: str = typer.Option( + ..., help="Wallet password.", prompt="Password", hide_input=True + ), +): + """ + Finalize an unfinalized transaction. + """ + + try: + token = session.token(wallet=wallet, password=password) + slatepack: str = console.input("Please, insert the Slatepack down below:\n") + if finalize_tx(session_token=token, slatepack=slatepack): + console.print("Transaction [bold]finalized[/bold] successfully ✔") + else: + error_console.print("Unable to finalized the transaction ✗") + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="receive") +def receive( + wallet: str = typer.Option( + ..., + help="Name of the wallet where you want to receive the coins.", + prompt="Wallet name", + ), + password: str = typer.Option( + ..., help="Wallet password.", prompt="Password", hide_input=True + ), +): + """ + Receive a transaction using Slatepack Messsage + """ + + try: + token = session.token(wallet=wallet, password=password) + if not psutil.WINDOWS: + import readline + + slatepack = console.input("Paste the Slatepack down below:\n") + + if len(slatepack.strip().rstrip()) == 0: + raise Exception("Empty Slatepack") + + signed_slatepack = receive_tx(session_token=token, slatepack=slatepack) + + console.print("\nPlease share the next Slatepack with the sender:") + console.print( + f"\n[yellow1]{signed_slatepack['slatepack'].strip().rstrip()}[yellow1]\n", + ) + console.print( + "*** The sender must know [bold]finalize[/bold] the transaction. ***", + style="italic", + ) + + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="post") +def post( + wallet: str = typer.Option( + ..., + help="Name of the wallet from which you wish to post the transaction.", + prompt="Wallet name", + ), + password: str = typer.Option( + ..., help="Wallet password.", prompt="Password", hide_input=True + ), + tx_id: int = typer.Option( + ..., + help="Id of the transaction you want to be repost", + prompt="Transaction ID", + ), +): + """ + Post a transaction to the network using the Id. + """ + + try: + token = session.token(wallet=wallet, password=password) + if post_tx(session_token=token, tx_id=tx_id): + console.print("Transaction [bold]posted[/bold] successfully ✔") + else: + error_console.print("Unable to post the transaction ✗") + + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="details") +def details( + wallet: str = typer.Option( + ..., help="Name of the wallet you want query", prompt="Wallet name" + ), + password: str = typer.Option( + ..., help="Wallet password.", prompt="Password", hide_input=True + ), + tx_id: int = typer.Option( + ..., + help="Id of the transaction you want to read", + prompt="Transaction Id", + ), + slatepack: bool = typer.Option(False, help="Return only the Slatepack"), + slate: bool = typer.Option(False, help="Export transaction Slate"), +): + """ + Get the information of a transaction using the Transaction Id. + """ + + details: dict = {} + + try: + session_token = session.token(wallet=wallet, password=password) + details = get_tx_details(session_token=session_token, tx_id=tx_id) + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + if slatepack: + console.print( + f"\n[yellow1]{details['armored_slatepack']}[yellow1]\n", + ) + raise typer.Exit() + + amount = 0 + fee = 0 + if "fee" in details: + fee = details["fee"] + + if details["amount_debited"] - details["amount_credited"] > 0: + amount = details["amount_credited"] - details["amount_debited"] + if "fee" in details: + amount = amount - fee + else: + amount = details["amount_debited"] - details["amount_credited"] + amount = abs(amount) + amount = amount / pow(10, 9) + fee = fee / pow(10, 9) + + table = Table(box=box.HORIZONTALS, expand=True, show_header=False) + table.add_column("", justify="right", style="bold") + table.add_column("", justify="left") + table.add_row("tx_id:", f"{details['id']}") + table.add_row("slate_id:", f"{details.get('slate_id','')}") + table.add_row("amount:", f"{amount:10,.9f} ツ") + table.add_row("fee:", f"{fee:10.9f} ツ") + table.add_row( + "created on:", f"{datetime.fromtimestamp(details['creation_date_time'])}" + ) + table.add_row("type:", f"{details['type'].lower()}") + if "confirmed_height" in details: + table.add_row("[bold]confirmed height:", f"{details['confirmed_height']}") + if details["kernels"] and details["kernels"] is not None: + kernels = "" + for kernel in details["kernels"]: + commitment = kernel["commitment"] + kernels = f"{commitment}" + table.add_row("kernels:", kernels) + if "outputs" in details and details["outputs"] is not None: + outputs = "" + for output in details["outputs"]: + commitment = output["commitment"] + keychain_path = output["keychain_path"] + status = output["status"] + outputs = f"[bold]commitment:[/bold] {commitment}\n[bold]keychain path:[/bold] {keychain_path}\n[bold]status:[/bold] {status.lower()}" + table.add_row("outputs:", outputs) + console.print(table) + + if slate: + file_name = f"{details['slate_id'].replace('-','_')}.json" + file_path = Path().resolve().joinpath(file_name) + with open(file_path, "w", encoding="utf-8") as f: + json.dump(details["slate"], f, ensure_ascii=True, indent=4) + console.print( + f"*[italic]Slate exported to path: [yellow1]{file_path}[yellow1]\n", + ) + + raise typer.Exit() + + +@app.command(name="decode") +def decode( + wallet: str = typer.Option( + ..., + help="Name of the wallet where you want to receive the coins.", + prompt="Wallet name", + ), + password: str = typer.Option( + ..., help="Wallet password.", prompt="Password", hide_input=True + ), +): + """ + decode a transaction using Slatepack Messsage + """ + + try: + token = session.token(wallet=wallet, password=password) + + slatepack = console.input("Paste the Slatepack down below:\n") + + if len(slatepack.strip().rstrip()) == 0: + raise Exception("Empty Slatepack") + + decoded_slatepack = decode_slatepack(session_token=token, message=slatepack) + + file_name = f"{decoded_slatepack['id'].replace('-','_')}.json" + file_path = Path().resolve().joinpath(file_name) + with open(file_path, "w", encoding="utf-8") as f: + json.dump(decoded_slatepack, f, ensure_ascii=True, indent=4) + console.print( + f"\n\n[bold]Slate exported to path: [italic yellow1]{file_path}[/italic yellow1]\n", + ) + + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() diff --git a/src/apps/transaction/nostr/__init__.py b/src/apps/transaction/nostr/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/apps/transaction/nostr/transport.py b/src/apps/transaction/nostr/transport.py new file mode 100644 index 0000000..cc74c7f --- /dev/null +++ b/src/apps/transaction/nostr/transport.py @@ -0,0 +1,368 @@ +#!/usr/bin/env python3 + +import json +import ssl +import time +import uuid +from datetime import datetime, timedelta +from typing import Optional + +import tornado.ioloop +import typer +from pynostr.base_relay import RelayPolicy +from pynostr.encrypted_dm import EncryptedDirectMessage +from pynostr.event import Event, EventKind +from pynostr.filters import Filters, FiltersList +from pynostr.key import PrivateKey +from pynostr.message_pool import EventMessage, MessagePool, NoticeMessage +from pynostr.message_type import ClientMessageType, RelayMessageType +from pynostr.relay import Relay +from pynostr.relay_manager import RelayManager +from pynostr.utils import get_public_key, get_timestamp +from rich import box +from rich.console import Console +from rich.prompt import Confirm, Prompt +from rich.table import Table +from tornado import gen + +from modules import nostr +from modules.api import TransactionsFilterOptions +from modules.api.owner.rpc import ( + add_initial_signature, + add_signature_to_transaction, + broadcast_transaction, + cancel_transaction, + estimate_transaction_fee, + get_transaction_details, + get_wallet_slatepack_address, + get_wallet_transactions, + send_coins, +) +from modules.wallet import session + +plugin = typer.Typer() + +console = Console(width=125, style="grey93") +error_console = Console(stderr=True, style="bright_red", width=125) + + +@plugin.command(name="send") +def send_tx_to_nostr( + wallet: str = typer.Option( + ..., + help="Name of the wallet from which you wish to get the transaction.", + prompt="Wallet name", + ), + password: str = typer.Option( + ..., help="Wallet password.", prompt="Password", hide_input=True + ), + id: int = typer.Option( + ..., + help="Id of the transaction you want to be repost", + prompt="Transaction ID", + ), + recipient_npub: str = typer.Option( + ..., + help="Recipient Public Key. npub or hex key.", + prompt="Recipient's npub key", + ), + relay_url: str = typer.Option( + ..., + help="The Nostr relay to connect to.", + prompt="Relay to connect to", + ), +): + """ + Send an unfinalized transaction through Nosrt + """ + + transaction: dict = {} + recipient = get_public_key(identity_str=recipient_npub) + + if not recipient: + error_console.print("Invalid recipient npub key") + raise typer.Abort() + + console.print( + f"Recipient Nostr public key: [dark_orange3]{recipient_npub}[/dark_orange3] (∩`-´)⊃━☆゚.*・。゚" + ) + + try: + session_token = session.token(wallet=wallet, password=password) + transaction = get_transaction_details(session_token=session_token, id=id) + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + console.print(f"Transaction with [bold]id={id}[/bold] found ✔") + + address = get_wallet_slatepack_address(session_token) + + wallet_raw_secret = nostr.generate_raw_secret( + wallet=wallet, address=address, password=password + ) + nostr_private_key = nostr.retrieve_private_key( + wallet=wallet, raw_secret=wallet_raw_secret + ) + + console.print(f"Sender [bold]Nostr key[/bold] loaded ✔") + + @gen.coroutine + def transaction_sent(message_json): + if message_json[0] == RelayMessageType.OK: + if len(message_json) == 4 and "blocked" in message_json[3]: + error_console.print(f" Error: {message_json[3]} ") + ascii = r""" +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░▄▄▄▄▄▄▄░░░░░░░░░ +░░░░░░░░░▄▀▀▀░░░░░░░▀▄░░░░░░░ +░░░░░░░▄▀░░░░░░░░░░░░▀▄░░░░░░ +░░░░░░▄▀░░░░░░░░░░▄▀▀▄▀▄░░░░░ +░░░░▄▀░░░░░░░░░░▄▀░░██▄▀▄░░░░ +░░░▄▀░░▄▀▀▀▄░░░░█░░░▀▀░█▀▄░░░ +░░░█░░█▄▄░░░█░░░▀▄░░░░░▐░█░░░ +░░▐▌░░█▀▀░░▄▀░░░░░▀▄▄▄▄▀░░█░░ +░░▐▌░░█░░░▄▀░░░░░░░░░░░░░░█░░ +░░▐▌░░░▀▀▀░░░░░░░░░░░░░░░░▐▌░ +░░▐▌░░░░░░░░░░░░░░░▄░░░░░░▐▌░ +░░▐▌░░░░░░░░░▄░░░░░█░░░░░░▐▌░ +░░░█░░░░░░░░░▀█▄░░▄█░░░░░░▐▌░ +░░░▐▌░░░░░░░░░░▀▀▀▀░░░░░░░▐▌░ +░░░░█░░░░░░░░░░░░░░░░░░░░░█░░ +░░░░▐▌▀▄░░░░░░░░░░░░░░░░░▐▌░░ +░░░░░█░░▀░░░░░░░░░░░░░░░░▀░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ + """ + console.print(ascii) + else: + console.print(f"Transaction [bold]sent[/bold] via Nostr ✔") + ascii = r""" +░░▄░░░▄░▄▄▄▄░░░░░░░░░░░░░░░ +░░█▀▄▀█░█▄▄░░░░░░░░░░░░░░░░ +░░█░░░█░█▄▄▄░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░▄▄▄░▄░░░▄░░▄▄▄░▄▄▄▄▄░░▄▄▄░ +█░░░░█░░░█░█░░░░░░█░░░█░░░█ +█░▀█░█░░░█░░▀▀▄░░░█░░░█▀▀▀█ +░▀▀▀░░▀▀▀░░▄▄▄▀░░░▀░░░▀░░░▀ + """ + console.print(ascii) + relay.close() + elif message_json[0] == RelayMessageType.NOTICE: + error_console.print(f"\nError: {message_json[1]} ¯\_(ツ)_/¯\n") + + relay.close() + + io_loop = tornado.ioloop.IOLoop.current() + relay = Relay( + url=relay_url, + message_pool=MessagePool(), + io_loop=io_loop, + policy=RelayPolicy(should_read=True, should_write=True), + close_on_eose=False, + message_callback=transaction_sent, + ) + + dm = EncryptedDirectMessage( + pubkey=nostr_private_key.public_key.hex(), + recipient_pubkey=recipient.hex(), + cleartext_content=transaction["armored_slatepack"], + ) + dm.encrypt( + private_key_hex=nostr_private_key.hex(), + ) + + event = dm.to_event() + + expiration_date = datetime.now() + timedelta(days=2) + event.add_tag("expiration", f"{int(expiration_date.timestamp())}") + + event.sign(nostr_private_key.hex()) + + with console.status("Sending transaction via Nostr..."): + relay.publish(event.to_message()) + try: + io_loop.run_sync(func=relay.connect) + except gen.Return: + pass + + raise typer.Exit() + + +@plugin.command(name="receive") +def grab_txs_from_nostr( + wallet: str = typer.Option( + ..., + help="Name of the wallet in which you want to insert transactions.", + prompt="Wallet name", + ), + password: str = typer.Option( + ..., help="Wallet password.", prompt="Password", hide_input=True + ), + relay_url: str = typer.Option( + ..., + help="The Nostr relay to connect to.", + prompt="Relay to connect to", + ), +): + """ + Grab pending transactions from Nostr + """ + + messages: list = [] + nostr_private_key: PrivateKey = None + + try: + session_token = session.token(wallet=wallet, password=password) + address = get_wallet_slatepack_address(session_token) + wallet_raw_secret = nostr.generate_raw_secret( + wallet=wallet, address=address, password=password + ) + nostr_private_key = nostr.retrieve_private_key( + wallet=wallet, raw_secret=wallet_raw_secret + ) + + if not nostr_private_key: + raise Exception("Nostr private not loaded") + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + console.print(f"Receiver [bold]Nostr key[/bold] loaded ✔") + + def decrypt_dm(message_json): + if message_json[0] == RelayMessageType.EVENT: + event = Event.from_dict(message_json[2]) + if event.kind == EventKind.ENCRYPTED_DIRECT_MESSAGE: + message = EncryptedDirectMessage.from_event(event) + message.decrypt( + private_key_hex=nostr_private_key.hex(), public_key_hex=event.pubkey + ) + messages.append( + { + "slatepack": message.cleartext_content, + "sender": event.pubkey, + "id": event.id, + } + ) + + filters = FiltersList( + [ + Filters( + kinds=[EventKind.ENCRYPTED_DIRECT_MESSAGE], + until=get_timestamp(), + pubkey_refs=[ + nostr_private_key.public_key.hex(), + ], + ) + ] + ) + + relay_manager = RelayManager(error_threshold=3, timeout=0) + relay_manager.add_relay( + url=relay_url, + close_on_eose=True, + message_callback=decrypt_dm, + ) + + subscription_id = uuid.uuid4().hex + relay_manager.add_subscription_on_all_relays(subscription_id, filters) + with console.status("Grabbing slatepacks..."): + relay_manager.run_sync() + relay_manager.close_subscription_on_all_relays(subscription_id) + relay_manager.close_all_relay_connections() + relay_manager.remove_closed_relays() + + console.print(f"Slatepacks found: [bold]{len(messages)}[/bold]") + if not messages: + raise typer.Exit() + + new_txs = [] + + for message in messages: + try: + message["signed_slate"] = add_initial_signature( + session_token=session_token, slatepack=message["slatepack"] + ) + new_txs.append(message) + except: + pass + + console.print(f"New Transactions received: [bold]{len(new_txs)}[/bold]") + if not new_txs: + raise typer.Exit() + + if Confirm.ask( + f"Would you like to review them and send back the signed Slatepack via Nostr?", + default=True, + ): + + def check_reply(message_json): + if message_json[0] == RelayMessageType.OK: + console.print(f"Slate sent via Nostr ✔") + elif message_json[0] == RelayMessageType.NOTICE: + error_console.print(f"\nError: {message_json[1]} ¯\_(ツ)_/¯\n") + + relay_manager.add_relay( + url=relay_url, + close_on_eose=True, + message_callback=check_reply, + policy=RelayPolicy(should_read=False, should_write=False), + ) + + subscription_id = uuid.uuid4().hex + relay_manager.add_subscription_on_all_relays(subscription_id, filters) + + for idx, item in enumerate(new_txs): + console.print("") + sender: str = "-" + if "sender" in item["signed_slate"]: + sender = item["signed_slate"]["sender"]["slatepack"] + amount: float = float(item["signed_slate"]["slate"]["amt"]) / pow(10, 9) + fee: float = float(item["signed_slate"]["slate"]["fee"]) / pow(10, 9) + table = Table( + box=box.HORIZONTALS, + expand=True, + show_header=False, + title=f"Slate Id: [bold]{item['signed_slate']['slate']['id']}[/bold]", + ) + table.add_column("", justify="right", style="bold") + table.add_column("", justify="left") + table.add_row("amount:", f"{amount:10,.9f} ツ") + table.add_row("fee:", f"{fee:10.9f} ツ") + table.add_row("sender:", f"{sender}") + console.print(f"[bold]Transaction #{idx+1}[/bold]") + console.print(table) + + if Confirm.ask( + f"Would you like to send back the signed Slatepack via Nostr?", + default=True, + ): + dm = EncryptedDirectMessage( + pubkey=nostr_private_key.public_key.hex(), + recipient_pubkey=item["sender"], + cleartext_content=item["signed_slate"]["slatepack"], + ) + dm.encrypt( + private_key_hex=nostr_private_key.hex(), + ) + + reply = dm.to_event() + + expiration_date = datetime.now() + timedelta(days=2) + reply.add_tag("expiration", f"{int(expiration_date.timestamp())}") + # create 'e' tag reference to the note you're replying to + reply.add_event_ref(item["id"]) + # create 'p' tag reference to the pubkey you're replying to + reply.add_pubkey_ref(item["sender"]) + reply.sign(nostr_private_key.hex()) + + relay_manager.publish_event(reply) + with console.status("Sending response..."): + relay_manager.run_sync() + + relay_manager.close_subscription_on_all_relays(subscription_id) + relay_manager.close_all_relay_connections() + relay_manager.remove_closed_relays() + + console.print("") diff --git a/src/apps/wallet/__init__.py b/src/apps/wallet/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/apps/wallet/app.py b/src/apps/wallet/app.py new file mode 100644 index 0000000..81bd5bf --- /dev/null +++ b/src/apps/wallet/app.py @@ -0,0 +1,438 @@ +#!/usr/bin/env python3 + +import typer +from rich import box +from rich.console import Console +from rich.prompt import Confirm +from rich.table import Table + +from modules import nostr + +from modules.utils import grinchck +from modules.wallet import session +from modules.api.owner.v3.wallet import ( + delete_wallet, + get_mnemonic, + get_top_level_directory, + list_wallets, + open_wallet, + close_wallet, + create_wallet, + restore_wallet, + retrieve_outputs, + retrieve_summary_info, + get_slatepack_address, +) + +app = typer.Typer() +console = Console(width=125, style="grey93") +error_console = Console(stderr=True, style="bright_red", width=125) + + +@app.command(name="open") +def open( + wallet: str = typer.Option( + ..., help="Name of the wallet you want to open", prompt=True + ), + password: str = typer.Option( + ..., + prompt=True, + hide_input=True, + help="Wallet password.", + ), +): + """ + Open a Wallet, then start the Tor Listener automatically. + """ + + try: + response = open_wallet(wallet=wallet, password=password) + console.print(f"Wallet [bold]{wallet}[/bold] opened ✔") + + if not session.store( + wallet=wallet, + token=response["session_token"], + password=password, + ): + close_wallet(response["session_token"]) + raise Exception("Unable to save session information ✗") + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="close") +def close( + wallet: str = typer.Option( + ..., help="Name of the wallet you want to close", prompt=True + ), + password: str = typer.Option( + ..., + prompt=True, + hide_input=True, + help="Wallet password.", + ), +): + """ + Close a running Wallet and stop the Tor Listener. + """ + + try: + session_token: str = session.token(wallet=wallet, password=password) + with console.status("closing walet..."): + close_wallet(session_token) + console.print(f"Wallet [bold]{wallet}[/bold] closed successfully ✔") + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="create") +def create( + name: str = typer.Option( + ..., help="Name of the wallet you want to create", prompt=True + ), + password: str = typer.Option( + ..., + prompt=True, + confirmation_prompt=True, + hide_input=True, + help="Wallet password.", + ), + words: int = typer.Option(24, help="Number of words for the mnemonic seed"), +): + """ + Create a new Wallet, open it and start the Tor Listener. + """ + + try: + response = create_wallet(wallet=name, password=password, mnemonic_length=words) + console.print(f"Wallet [bold]{name}[/bold] created ✔") + console.print(f"Wallet seed phrase: [bold]{response['wallet_seed']}[/bold] ✇") + + if not session.store( + wallet=name, + token=response["session_token"], + password=password, + ): + close_wallet(response["session_token"]) + raise Exception("Unable to save session information ✗") + + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="recover") +def recover( + name: str = typer.Option( + ..., help="Name for the wallet you want to recover", prompt=True + ), + seed: str = typer.Option(..., help="Seed phrase", prompt=True), + password: str = typer.Option( + ..., + prompt=True, + confirmation_prompt=True, + hide_input=True, + help="Wallet password.", + ), +): + """ + Recover a Wallet using the seed phrase. + """ + + try: + response = restore_wallet(wallet=name, password=password, mnemonic=seed) + console.print(f"Wallet [bold]{name}[/bold] recovered ✔") + + if not session.store( + wallet=name, + token=response["session_token"], + password=password, + ): + raise Exception("Unable to save session information ✗") + + console.print(f"Session information created ✔") + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="delete") +def delete( + name: str = typer.Option( + ..., help="Name of the wallet you want to delete", prompt=True + ), + password: str = typer.Option( + ..., prompt=True, hide_input=True, help="Wallet password." + ), +): + """ + Delete a Wallet. + """ + + if Confirm.ask("Are you sure you want to delete the wallet?", default=False): + try: + seed = get_mnemonic(wallet=name, password=password) + console.print( + f"Wallet seed phrase: [bold italic]{seed}[/bold italic]", style="" + ) + + delete_wallet(wallet=name, password=password) + + console.print(f"Wallet [bold]{name}[/bold] deleted") + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="seed") +def backup( + wallet: str = typer.Option(..., help="Name of the wallet you want to backup."), + password: str = typer.Option( + ..., prompt=True, hide_input=True, help="Wallet password." + ), +): + """ + Export the Seed the Wallet. + """ + + try: + seed = get_mnemonic(wallet=wallet, password=password) + console.print(f"Wallet seed phrase: [bold white]{seed}[/bold white]") + + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="list") +def wallet_list(): + """ + List the created Wallets. + """ + + try: + wallets = list_wallets() + if wallets: + table = Table(title="Wallets", box=box.HORIZONTALS, expand=True) + table.add_column("") + table.add_column("name") + i = 1 + for wallet in wallets: + table.add_row( + f"{i}", + f"[bold yellow]{wallet}", + ) + i += 1 + + console.print(table) + else: + console.print("No wallet found") + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="balance") +def get_balance( + wallet: str = typer.Option( + ..., help="Name of the wallet you want to check", prompt="Wallet name" + ), + password: str = typer.Option( + ..., + prompt=True, + hide_input=True, + help="Wallet password.", + ), +): + """ + Get the balance of a running Wallet. + """ + + try: + token = session.token(wallet=wallet, password=password) + + balance = retrieve_summary_info(token) + + table = Table( + title="Wallet's Balance", + box=box.HORIZONTALS, + show_footer=True, + show_header=True, + expand=True, + ) + + table.add_column("", "Total", justify="right") + table.add_column( + "amount ツ", f'{balance["total"] / pow(10, 9):10,.9f} ', justify="right" + ) + table.add_row( + "Spendable:", + f'[green3]{balance["spendable"] / pow(10, 9):10,.9f}', + style="bold", + ) + table.add_row( + "Immature:", f'[dark_orange3]{balance["immature"] / pow(10, 9):10,.9f}' + ) + table.add_row( + "Unconfirmed:", f'[gold1]{balance["unconfirmed"] / pow(10, 9):10,.9f}' + ) + table.add_row( + "Locked:", f'[bright_black]{balance["locked"] / pow(10, 9):10,.9f}' + ) + + console.print(table) + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="address") +def wallet_address( + wallet: str = typer.Option( + ..., help="Name of the opened wallet you want to query", prompt="Wallet" + ), + password: str = typer.Option( + ..., + prompt=True, + hide_input=True, + help="Wallet password.", + ), + test: bool = typer.Option( + False, help="Check if wallet address is reachable via Tor" + ), + npub: bool = typer.Option(False, help="Print the Nostr Public Key"), +): + """ + Get the Slatepack Address of a running Wallet. + """ + + try: + session_token = session.token(wallet=wallet, password=password) + address = get_slatepack_address(session_token) + console.print(f"Slatepack Address: [green3]{address}[/green3]") + + if test: + reachable = grinchck.connect( + slatepack_address=address["slatepack"], + api_url="http://192.227.214.130/", + ) + if reachable: + console.print( + f"Address [green3]{address}[/green3] is reachable via the Tor Network <(^_^)>" + ) + else: + error_console.print( + f"Addres [dark_orange]{address}[/dark_orange] is not reachable the Tor Network \_(-_-)_/" + ) + if npub: + wallet_raw_secret = nostr.generate_raw_secret( + wallet=wallet, address=address["slatepack"], password=password + ) + nostr_private_key = nostr.retrieve_private_key( + wallet=wallet, raw_secret=wallet_raw_secret + ) + console.print( + f"Nostr' Public Key: [dark_orange3]{nostr_private_key.public_key}[/dark_orange3] (∩`-´)⊃━☆゚.*・。゚" + ) + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="outputs") +def list_outputs( + wallet: str = typer.Option( + ..., help="Name of the wallet you want to check", prompt="Wallet name" + ), + password: str = typer.Option( + ..., + prompt=True, + hide_input=True, + help="Wallet password.", + ), +): + """ + Get the balance of a running Wallet. + """ + + try: + token = session.token(wallet=wallet, password=password) + + outputs = retrieve_outputs(token) + + table = Table( + title="Wallet's Outputs", + box=box.HORIZONTALS, + show_footer=True, + show_header=True, + expand=True, + ) + table = Table(title="", box=box.HORIZONTALS, expand=True) + table.add_column("amount ツ", justify="right", width=15) + table.add_column("commitment", justify="center") + table.add_column("status", justify="center", width=10) + table.add_column("tx_id", justify="center") + table.add_column("explorer", justify="center") + + for output in outputs: + amount = output["amount"] / pow(10, 9) + link = f"[link=https://grinexplorer.net/output/{output['commitment']}]Open[/link]" + table.add_row( + f"{amount:10,.9f}", + f"{output['commitment']}", + f"{output['status']}", + f"{output['transaction_id']}", + link, + ) + console.print(table) + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="directory") +def get_directory( + wallet: str = typer.Option( + ..., help="Name of the wallet you want query", prompt="Wallet name" + ), + password: str = typer.Option( + ..., help="Wallet password.", prompt="Password", hide_input=True + ), +): + """ + Get the information of a transaction using the Transaction Id. + """ + + try: + session_token = session.token(wallet=wallet, password=password) + folder = get_top_level_directory(session_token=session_token) + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + console.print( + f"*[italic]Slate exported to path: [yellow1]{folder}[yellow1]\n", + ) + raise typer.Exit() diff --git a/src/cli.py b/src/cli.py new file mode 100644 index 0000000..386f24e --- /dev/null +++ b/src/cli.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 + +import os +from pathlib import Path +from typing import Optional + +import typer +from rich.console import Console + +from apps.misc import app as misc_app +from apps.node import app as node_app +from apps.transaction import app as transaction_app +from apps.wallet import app as wallet_app + +__appname__ = "Grin++ CLI" +__version__ = "0.2.0" + + +Path(Path.home().joinpath(os.getenv("GRINPP_CLI_DATA_PATH", ".grinplusplus"))).mkdir( + parents=True, exist_ok=True +) + + +def version_callback(value: bool): + if value: + typer.echo(f"{__appname__} v{__version__}") + raise typer.Exit() + + +def main( + debug: bool = typer.Option( + default=False, + help="Enable the verbose mode for troubleshooting purposes.", + ), + json: bool = typer.Option( + default=False, + help="Return json instead", + ), + version: Optional[bool] = typer.Option( + None, + "--version", + callback=version_callback, + is_eager=True, + help="Return current version.", + ), +): + """ + Grin++: Fast, Private and Secure Grin Wallet. + + Grin is a lightweight implementation of the Mimblewimble protocol. The main goals and features + of the Grin project are: Privacy, Scalability, Simplicity, Simple Cryptography and + Decentralization. Grin wants to be usable by everyone, regardless of borders, culture, + capabilities or access. To learn more about Grin, visit GRIN.MW. + """ + if debug: + state["verbose"] = True + if json: + state["json"] = True + + +cli = typer.Typer( + callback=main, + no_args_is_help=True, + epilog="If you need support, please join the Grin++ group on Telegram: https://t.me/GrinPP", +) +state = {"verbose": False, "json": False} +console = Console(width=125, style="grey93") +error_console = Console(stderr=True, style="bright_red", width=125) + +cli.add_typer( + misc_app.app, + name="misc", + no_args_is_help=True, + help="Miscellaneous cool things! Give it a try ʘ‿ʘ", +) + +cli.add_typer( + wallet_app.app, + name="wallet", + no_args_is_help=True, + help="Wallet management commands. Use this set of commands to create, open and manage your wallets.", +) + +cli.add_typer( + node_app.app, + name="node", + no_args_is_help=True, + help="Manage the status of the local Grin++ node. Launch, stop, (re)Sync, and many more.", +) + +cli.add_typer( + transaction_app.app, + name="transaction", + no_args_is_help=True, + help="Execute all actions regarding transactions. These actions require to be executed upon an open wallet.", +) + + +if __name__ == "__main__": + cli() diff --git a/src/modules/__init__.py b/src/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/api/__init__.py b/src/modules/api/__init__.py new file mode 100644 index 0000000..d9258ce --- /dev/null +++ b/src/modules/api/__init__.py @@ -0,0 +1,28 @@ +from enum import Enum + +import psutil + +from modules.utils.processes import find + + +class TransactionsFilterOptions(str, Enum): + coinbase = "coinbase" + sent = "sent" + pending = "pending" + received = "received" + canceled = "canceled" + all = "all" + + +def _get_process_status_error() -> str: + if not _is_node_running(): + error = "Grin node is not running" + return "Unable to connect to the node" + + +def _is_node_running() -> bool: + node_process_name = "GrinNode" + if psutil.WINDOWS: + node_process_name = "GrinNode.exe" + + return len(find(node_process_name)) > 0 diff --git a/src/modules/api/foreign/__init__.py b/src/modules/api/foreign/__init__.py new file mode 100644 index 0000000..6369fe8 --- /dev/null +++ b/src/modules/api/foreign/__init__.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 + +import uuid + +import requests + +from modules.api import _get_process_status_error + + +def _owner_foreign_rpc_call(method: str, params: dict = {}) -> dict: + owner_foreign_rpc_url = "http://127.0.0.1:3413/v2/foreign" + + call_params = { + "jsonrpc": "2.0", + "method": method, + "id": str(uuid.uuid4()), + "params": params, + } + + try: + data: dict = requests.post(url=owner_foreign_rpc_url, json=call_params).json() + if "error" in data: + raise Exception(data["error"]["message"]) + return data + except requests.exceptions.ConnectionError: + raise Exception(_get_process_status_error()) diff --git a/src/modules/api/foreign/rpc.py b/src/modules/api/foreign/rpc.py new file mode 100644 index 0000000..710474c --- /dev/null +++ b/src/modules/api/foreign/rpc.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 + +from modules.api.foreign import _owner_foreign_rpc_call + + +def get_list_of_settings() -> dict: + return _owner_foreign_rpc_call("get_config") diff --git a/src/modules/api/node/__init__.py b/src/modules/api/node/__init__.py new file mode 100644 index 0000000..d354065 --- /dev/null +++ b/src/modules/api/node/__init__.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 + +import requests + +from modules.api import _get_process_status_error + + +def _node_rest_call(method: str, params: str = "") -> dict: + node_rest_api_url = f"http://127.0.0.1:3413/v1/{method}{params}" + try: + return requests.get(url=node_rest_api_url).json() + except requests.exceptions.ConnectionError: + raise Exception(_get_process_status_error()) + + +""" +{ + "chain": { + "hash": "000181a8336f6ae49ba314083c3e5a817a335616e0e0ad204db28733fd825b52", + "height": 2266638, + "previous_hash": "000371e99eb51d745c38925960c304a5c681f9cefc19c941fa72befd6d567a36", + "total_difficulty": 2064958700115730 + }, + "header_height": 2268377, + "network": { + "height": 2268379, + "num_inbound": 0, + "num_outbound": 5, + "total_difficulty": 2065383457733505 + }, + "protocol_version": 1000, + "state": { + "download_size": 0, + "downloaded": 0, + "processing_status": 0 + }, + "sync_status": "SYNCING_BLOCKS", + "user_agent": "Grin++ 1.2.8" +} + +{ + "chain": { + "hash": "000226d54b1db6763bb26744cbfb4aa172d66c2697a57dcff04f5662001b4697", + "height": 2268385, + "previous_hash": "0002dce631821930aa8c051042b0919f281279d86ae846df1d1a7d58ed95dd4e", + "total_difficulty": 2065384900184026 + }, + "header_height": 2268385, + "network": { + "height": 2268385, + "num_inbound": 0, + "num_outbound": 7, + "total_difficulty": 2065384900184026 + }, + "protocol_version": 1000, + "state": { + "download_size": 0, + "downloaded": 0, + "processing_status": 0 + }, + "sync_status": "FULLY_SYNCED", + "user_agent": "Grin++ 1.2.8" +} +""" + + +def _parse_status_response(data: dict) -> tuple[str, float]: + message: str = "" + percentage: float = 0.0 + + if data["sync_status"] == "NOT_CONNECTED": + message = "Waiting for Peers" + elif data["sync_status"] == "FULLY_SYNCED": + message = "Running" + percentage = 100 * float(data["chain"]["height"]) / float(data["header_height"]) + elif data["sync_status"] == "SYNCING_HEADERS": + message = "1/4 Syncing Headers" + if data["network"]["height"] > 0: + percentage = ( + 100 * float(data["header_height"]) / float(data["network"]["height"]) + ) + else: + percentage = 0 + elif data["sync_status"] == "DOWNLOADING_TXHASHSET": + message = "2/4 Downloading State" + if data["state"]["download_size"] > 0: + percentage = ( + 100 + * float(data["state"]["downloaded"]) + / float(data["state"]["download_size"]) + ) + else: + percentage = 0 + elif data["sync_status"] == "PROCESSING_TXHASHSET": + message = "3/4 Validating State" + percentage = data["state"]["processing_status"] + elif data["sync_status"] == "SYNCING_BLOCKS": + message = "4/4 Syncing Blocks" + if data["chain"]["height"] == 0 or data["chain"]["height"] == 0: + percentage = 0 + elif data["header_height"] < 10080 or data["chain"]["height"] < 10080: + percentage = ( + 100 * float(data["chain"]["height"]) / float(data["header_height"]) + ) + elif data["header_height"] - data["chain"]["height"] > 10080: + percentage = 0 + else: + remaining = 100 * ( + (float(data["header_height"]) - float(data["chain"]["height"])) / 10080 + ) + if remaining <= 0: + remaining = 1 + percentage = 100 - remaining + + return message, percentage diff --git a/src/modules/api/node/rest.py b/src/modules/api/node/rest.py new file mode 100644 index 0000000..ce42fd6 --- /dev/null +++ b/src/modules/api/node/rest.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 + +from modules.api import _is_node_running +from modules.api.node import _node_rest_call, _parse_status_response + + +def get_node_state() -> dict: + return _node_rest_call("status") + + +def get_sync_state() -> tuple[str, float]: + return _parse_status_response(get_node_state()) + + +def get_node_connected_peers() -> list: + peers: list = [] + peer: dict + for peer in _node_rest_call("peers", "/connected"): + peers.append(peer) + return peers + + +def shutdown_node() -> bool: + _node_rest_call("shutdown") + return _is_node_running() == False diff --git a/src/modules/api/owner/__init__.py b/src/modules/api/owner/__init__.py new file mode 100644 index 0000000..3adf2cb --- /dev/null +++ b/src/modules/api/owner/__init__.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 + +import uuid + +import requests + +from modules.api import _get_process_status_error + + +def _owner_wallet_rpc_call(method: str, params: dict = {}) -> dict: + owner_wallet_rpc_url = "http://127.0.0.1:3421/v2" + + call_params = { + "jsonrpc": "2.0", + "method": method, + "id": str(uuid.uuid4()), + "params": params, + } + + try: + response: requests.Response = requests.post( + url=owner_wallet_rpc_url, json=call_params + ) + if len(response.text) > 0: + data = response.json() + if "error" in data: + raise Exception(data["error"]["message"]) + return data["result"] + return {} + except requests.exceptions.ConnectionError: + raise Exception(_get_process_status_error()) + + +def _filter_transactions_by_status(transactions: list, status: str): + transactions.sort(key=lambda t: t["creation_date_time"], reverse=True) + + if status == "coinbase": + return filter(lambda t: "coinbase" in str(t["type"]).lower(), transactions) + elif status == "sent": + return filter(lambda t: "sent" in str(t["type"]).lower(), transactions) + elif status == "pending": + return filter( + lambda t: "sending" in str(t["type"]).lower() + or "receiving" in str(t["type"]).lower(), + transactions, + ) + elif status == "received": + return filter(lambda t: str(t["type"]).lower() == "received", transactions) + elif status == "canceled": + return filter(lambda t: "canceled" in str(t["type"]).lower(), transactions) + + return transactions diff --git a/src/modules/api/owner/rpc.py b/src/modules/api/owner/rpc.py new file mode 100644 index 0000000..510639f --- /dev/null +++ b/src/modules/api/owner/rpc.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 + +from typing import Literal + +from modules.api.owner import _filter_transactions_by_status, _owner_wallet_rpc_call + + +def get_list_of_wallets() -> list[str]: + wallets: list[str] = [] + wallet: str + for wallet in _owner_wallet_rpc_call("list_wallets", {}): + wallets.append(wallet) + return wallets + + +def open_wallet_by_name(name: str, password: str): + return _owner_wallet_rpc_call("login", {"username": name, "password": password}) + + +def get_wallet_transactions( + session_token: str, + status: Literal["all", "coinbase", "sent", "pending", "received", "canceled"], +) -> list: + transactions: list = _owner_wallet_rpc_call( + "list_txs", {"session_token": session_token} + )["txs"] + return list(_filter_transactions_by_status(transactions, status)) + + +def close_wallet_by_name(session_token: str) -> None: + _owner_wallet_rpc_call("logout", {"session_token": session_token}) + return + + +def get_wallet_balance(session_token: str): + return _owner_wallet_rpc_call("get_balance", {"session_token": session_token}) + + +def get_wallet_slatepack_address(session_token: str) -> str: + response: dict = _owner_wallet_rpc_call( + "get_slatepack_address", {"session_token": session_token} + ) + return response["slatepack"] + + +def get_wallet_seed(wallet: str, password: str) -> str: + response: dict = _owner_wallet_rpc_call( + "get_wallet_seed", {"username": wallet, "password": password} + ) + return response["wallet_seed"] + + +def delete_wallet_by_name(wallet: str, password: str) -> bool: + response: dict = _owner_wallet_rpc_call( + "delete_wallet", {"username": wallet, "password": password} + ) + return response["status"] == "SUCCESS" + + +def create_wallet(wallet: str, password: str, words: int) -> dict: + return _owner_wallet_rpc_call( + "create_wallet", + { + "username": wallet, + "password": password, + "num_seed_words": words, + }, + ) + + +def estimate_transaction_fee(session_token: str, amount: float) -> dict: + params: dict = { + "session_token": session_token, + "amount": amount * pow(10, 9), + "fee_base": 500000, + "change_outputs": 1, + "selection_strategy": {"strategy": "SMALLEST"}, + } + return _owner_wallet_rpc_call( + "estimate_fee", + params, + ) + + +def send_coins( + session_token: str, + amount: float, + address: str = "", +) -> dict: + params: dict = { + "session_token": session_token, + "address": address, + "amount": amount * pow(10, 9), + "fee_base": 500000, + "change_outputs": 1, + "selection_strategy": {"strategy": "SMALLEST", "inputs": []}, + "post_tx": {"method": "FLUFF"}, + } + return _owner_wallet_rpc_call("send", params) + + +def cancel_transaction(session_token: str, id: int) -> dict: + response: dict = _owner_wallet_rpc_call( + "cancel_tx", + { + "session_token": session_token, + "tx_id": id, + }, + ) + return response["status"] == "SUCCESS" + + +def add_signature_to_transaction(session_token: str, slatepack: str) -> dict: + response: dict = _owner_wallet_rpc_call( + "finalize", + { + "session_token": session_token, + "slatepack": slatepack.rstrip().strip(), + }, + ) + return response["status"] == "FINALIZED" + + +def add_initial_signature(session_token: str, slatepack: str) -> dict: + return _owner_wallet_rpc_call( + "receive", + { + "session_token": session_token, + "slatepack": slatepack.rstrip().strip(), + }, + ) + + +def broadcast_transaction(session_token: str, id: int) -> bool: + response = _owner_wallet_rpc_call( + "repost_tx", + {"session_token": session_token, "tx_id": id, "method": "FLUFF"}, + ) + if "status" in response: + return response["status"] != "FAILED" + return True + + +def get_transaction_details(session_token: str, id: int) -> dict: + transactions = _owner_wallet_rpc_call("list_txs", {"session_token": session_token}) + + return next((x for x in transactions["txs"] if x["id"] == id)) + + +def restore_wallet(wallet: str, password: str, seed: str) -> dict: + return _owner_wallet_rpc_call( + "restore_wallet", + { + "username": wallet, + "password": password, + "wallet_seed": seed.strip(), + }, + ) diff --git a/src/modules/api/owner/v2/__init__.py b/src/modules/api/owner/v2/__init__.py new file mode 100644 index 0000000..384724b --- /dev/null +++ b/src/modules/api/owner/v2/__init__.py @@ -0,0 +1,18 @@ +from uuid import uuid4 + +import requests + + +def call_owner_rpc_v2(method: str, params: dict = {}) -> dict: + url = "http://127.0.0.1:3413/v2/owner" + + params = {"jsonrpc": "2.0", "method": method, "id": str(uuid4()), "params": params} + + response = requests.post(url=url, json=params) + + data = response.json() + + if "error" in data: + raise Exception(data["error"]["message"]) + + return data["result"]["Ok"] diff --git a/src/modules/api/owner/v2/node.py b/src/modules/api/owner/v2/node.py new file mode 100644 index 0000000..4e813c3 --- /dev/null +++ b/src/modules/api/owner/v2/node.py @@ -0,0 +1,87 @@ +from modules.api.owner.v2 import call_owner_rpc_v2 + + +def parse_node_status(status: dict) -> tuple[str, float]: + """ + Parse status response in order to get a human-like messange and a percentage. + + Returns + ------ + tuple[str, float] + A tuple containing the huma-like message and percentage. + """ + message = "Waiting for Peers" + percentage = 0.0 + + if status["sync_status"] == "no_sync": + message = "Running" + percentage = 100 + + elif status["sync_status"] == "header_sync": + message = "1/7 Syncing Headers" + if status["sync_info"]["highest_height"] > 0: + percentage = ( + status["sync_info"]["current_height"] + * 100 + / status["sync_info"]["highest_height"] + ) + + elif status["sync_status"] == "txhashset_download": + message = "2/7 Downloading Chain State" + if status["sync_info"]["downloaded_size"] > 0: + percentage = ( + status["sync_info"]["downloaded_size"] + * 100 + / status["sync_info"]["total_size"] + ) + elif status["sync_status"] == "syncing": + message = "Preparing Chain State for Validation" + + elif status["sync_status"] == "txhashset_rangeproofs_validation": + message = "3/7 Validating Chain State" + elif status["sync_status"] == "txhashset_kernels_validation": + message = "4/7 Validating Chain State" + elif status["sync_status"] == "txhashset_kernels_validation": + message = "4/7 Validating Chain State" + + elif status["sync_status"] == "txhashset_processing": + message = "5/7 Validating Chain State" + + elif status["sync_status"] == "body_sync": + message = "7/7 Syncing Blocks" + if status["sync_info"]["highest_height"] > 0: + percentage = ( + status["sync_info"]["current_height"] + * 100 + / status["sync_info"]["highest_height"] + ) + + return message, percentage + + +def get_status() -> dict: + """ + Get various information about the node, the network and the current sync status. + + Returns + ------ + dict + A dict containing protocol_version, user_agent, connections, tip and + sync_status. + """ + result = call_owner_rpc_v2("get_status") + + return result + + +def get_connected_peers() -> dict: + """ + Retrieves a list of all connected peers. + + Returns + ------ + dict + A list containing peers. + """ + result = call_owner_rpc_v2("get_connected_peers") + return result diff --git a/src/modules/api/owner/v3/__init__.py b/src/modules/api/owner/v3/__init__.py new file mode 100644 index 0000000..04b62bf --- /dev/null +++ b/src/modules/api/owner/v3/__init__.py @@ -0,0 +1,20 @@ +from uuid import uuid4 +from rich import print +import requests + + +def call_owner_rpc_v3(method: str, params: dict = {}) -> dict: + url = "http://127.0.0.1:3420/v3/owner" + + params = { + "jsonrpc": "2.0", + "method": method, + "id": str(uuid4()), + "params": params, + } + + response = requests.post(url=url, json=params).json() + if "error" in response: + raise Exception(response["error"]["message"]) + ##print(response["result"]) + return response["result"]["Ok"] diff --git a/src/modules/api/owner/v3/helpers.py b/src/modules/api/owner/v3/helpers.py new file mode 100644 index 0000000..5f3b34e --- /dev/null +++ b/src/modules/api/owner/v3/helpers.py @@ -0,0 +1,27 @@ +def filter_transactions(transactions: list[dict], status: str) -> list: + transactions.sort(key=lambda t: t["creation_date_time"], reverse=False) + + if status == "coinbase": + return list( + filter(lambda t: "coinbase" in str(t["type"]).lower(), transactions) + ) + elif status == "sent": + return list(filter(lambda t: "sent" in str(t["type"]).lower(), transactions)) + elif status == "pending": + return list( + filter( + lambda t: "sending" in str(t["type"]).lower() + or "receiving" in str(t["type"]).lower(), + transactions, + ) + ) + elif status == "received": + return list( + filter(lambda t: str(t["type"]).lower() == "received", transactions) + ) + elif status == "canceled": + return list( + filter(lambda t: "canceled" in str(t["type"]).lower(), transactions) + ) + + return transactions diff --git a/src/modules/api/owner/v3/wallet.py b/src/modules/api/owner/v3/wallet.py new file mode 100644 index 0000000..28ec725 --- /dev/null +++ b/src/modules/api/owner/v3/wallet.py @@ -0,0 +1,147 @@ +from modules.api.owner.v3 import call_owner_rpc_v3 as call +from modules.api.owner.v3.helpers import filter_transactions + + +def list_wallets() -> list[str]: + wallets: list[str] = [] + for wallet in call("list_wallets", {}): + wallets.append(wallet) + return wallets + + +def open_wallet( + wallet: str, + password: str, +) -> dict: + return call("open_wallet", {"name": wallet, "password": password}) + + +def close_wallet(session_token: str) -> dict: + return call("close_wallet", {"session_token": session_token}) + + +def create_wallet(wallet: str, password: str, mnemonic_length: int = 24) -> dict: + return call( + "create_wallet", + { + "username": wallet, + "password": password, + "mnemonic_length": mnemonic_length, + }, + ) + + +def get_mnemonic( + wallet: str, + password: str, +) -> dict: + return call("get_mnemonic", {"name": wallet, "password": password}) + + +def delete_wallet( + wallet: str, + password: str, +) -> dict: + return call("delete_wallet", {"name": wallet, "password": password}) + + +def restore_wallet(wallet: str, password: str, mnemonic: str) -> dict: + return call( + "create_wallet", + { + "username": wallet, + "password": password, + "mnemonic": mnemonic, + }, + ) + + +def retrieve_summary_info(session_token: str) -> dict: + return call("retrieve_summary_info", {"session_token": session_token}) + + +def get_slatepack_address(session_token: str) -> dict: + return call("get_slatepack_address", {"session_token": session_token}) + + +def retrieve_txs(session_token: str, status: str) -> list: + result = call("retrieve_txs", {"session_token": session_token}) + return filter_transactions(result[1], status) + + +def get_stored_tx(session_token: str, slate_id: str) -> dict: + return call("get_stored_tx", {"session_token": session_token, "slate_id": slate_id}) + + +def slate_from_slatepack_message(session_token: str) -> dict: + return call("slate_from_slatepack_message", {"session_token": session_token}) + + +def get_tx_details(session_token: str, tx_id: int) -> dict: + return call("get_tx_details", {"session_token": session_token, "tx_id": tx_id}) + + +def retrieve_outputs(session_token: str) -> dict: + return call("retrieve_outputs", {"session_token": session_token}) + + +def get_top_level_directory(session_token: str) -> dict: + return call("get_top_level_directory", {"session_token": session_token}) + + +def post_tx(session_token: str, tx_id: int) -> dict: + return call( + "post_tx", {"session_token": session_token, "tx_id": tx_id, "method": "FLUFF"} + ) + + +def estimate_fee( + session_token: str, + amount: float = -1, +) -> dict: + params: dict = { + "session_token": session_token, + "fee_base": 500000, + "selection_strategy": {"strategy": "SMALLEST", "inputs": []}, + } + if amount > 0: + params["amount"] = str(amount * pow(10, 9)) + + return call("estimate_fee", params) + + +def send_tx( + session_token: str, + amount: float = -1, + address: str = "", +) -> dict: + params: dict = { + "session_token": session_token, + "address": address, + "fee_base": 500000, + "change_outputs": 1, + "selection_strategy": {"strategy": "SMALLEST", "inputs": []}, + "post_tx": {"method": "FLUFF"}, + } + if amount > 0: + params["amount"] = str(amount * pow(10, 9)) + + return call("send", params) + + +def cancel_tx(session_token: str, tx_id: int) -> dict: + return call("cancel_tx", {"session_token": session_token, "tx_id": tx_id}) + + +def receive_tx(session_token: str, slatepack: str) -> dict: + return call("receive_tx", {"session_token": session_token, "slatepack": slatepack}) + + +def decode_slatepack(session_token: str, message: str) -> dict: + return call( + "decode_slatepack_message", {"session_token": session_token, "message": message} + ) + + +def finalize_tx(session_token: str, slatepack: str) -> dict: + return call("finalize_tx", {"session_token": session_token, "slatepack": slatepack}) diff --git a/src/modules/nostr/__init__.py b/src/modules/nostr/__init__.py new file mode 100644 index 0000000..da12fda --- /dev/null +++ b/src/modules/nostr/__init__.py @@ -0,0 +1,132 @@ +import os +from pathlib import Path +from typing import Optional + +from cryptography.fernet import Fernet +from pynostr.key import PrivateKey + +from modules.utils.kdf import derive_key, encode_key + + +def _derive_passphrase(password: str): + """ + Derive a key using a passphrase. The method will try to get an already stored `salt` or generate a new one if necessary. + + Parameters + ---------- + password : str + Password of the wallet. + + Returns + ------- + bytes + Raw bytes of the derived key. + """ + return derive_key(passphrase=password.encode()) + + +def _generate_private_key(nsec: Optional[str] = None) -> PrivateKey: + """ + Generates a Nostr Private Key. It will use an nsec if passed as a parameter. + + Parameters + ---------- + nsec : str + An already generate nsec. It will create a completely new private if nsec is + None. + + Returns + ------- + PrivateKey + A Nostr Private Key. + """ + if nsec: + return PrivateKey.from_nsec(nsec=nsec) + return PrivateKey() + + +def _store_private_key(raw_secret: bytes, key_path: Path) -> PrivateKey: + """ + Save a Nostr Privat Key inside `key_path`. The content is encrypted using + `raw_secret`. + + Parameters + ---------- + raw_secret: bytes + Secret key used to decrypt/encrypt the line with the nsec. + key_path : Path + Walle name. This is uses to name the file like: `[wallet].nostr`. + + Returns + ------ + PrivateKey + A Nostr Private Key. + """ + private_key = _generate_private_key() + with open(key_path, "wb+") as file: + file.write( + Fernet(encode_key(raw_secret)).encrypt(private_key.bech32().encode()) + ) + if key_path.exists(): + return private_key + raise Exception("Unable to store generated Nostr Private Key") + + +def generate_raw_secret(wallet: str, address: str, password: str) -> bytes: + """ + Get a raw secret based on wallet address to encrypt/decrypt the file containing the + nsec. + + Parameters + ---------- + wallet : str + Name of the wallet. + address : str + Slatepack Address of the wallet. + password : str + Password of the wallet. + + Returns + ------- + bytes + Raw bytes of the derived key. + """ + """ + Returns a tuple: Nostr Private Key and raw bytes + """ + return _derive_passphrase(f"{wallet}{address}{password}") + + +def retrieve_private_key(wallet: str, raw_secret: bytes) -> PrivateKey: + """ + Returns the corresponding Nostr Private Key of a opened wallet. This will create a + new Private Key in case the contents of the file containing the nsec cannot be + decrypted. + + Parameters + ---------- + wallet : str + Walle name. This is uses to name the file like: `[wallet].nostr`. + raw_secret: bytes + Secret key used to decrypt/encrypt the line with the nsec. + + Returns + ------ + PrivateKey + A Nostr Private Key. + """ + + key_path: Path = Path.home().joinpath( + os.getenv("GRINPP_CLI_DATA_PATH", ".grinplusplus"), f"{wallet}.nostr" + ) + if key_path.exists(): + file = open(key_path, "rb") + nostr_key = file.read() + file.close() + try: + nsec = Fernet(encode_key(raw_secret)).decrypt(nostr_key).decode() + return _generate_private_key(nsec=nsec) + except: + return _store_private_key(raw_secret, key_path) + + return _store_private_key(raw_secret, key_path) diff --git a/src/modules/utils/__init__.py b/src/modules/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/utils/coingecko.py b/src/modules/utils/coingecko.py new file mode 100644 index 0000000..6f37502 --- /dev/null +++ b/src/modules/utils/coingecko.py @@ -0,0 +1,123 @@ +""" +This modules defines some useful tools to use with the CoinGecko API. +""" + +import requests + +SUPPORTED_CURRENCIES = [ + "btc", + "eth", + "ltc", + "bch", + "bnb", + "eos", + "xrp", + "xlm", + "link", + "dot", + "yfi", + "usd", + "aed", + "ars", + "aud", + "bdt", + "bhd", + "bmd", + "brl", + "cad", + "chf", + "clp", + "cny", + "czk", + "dkk", + "eur", + "gbp", + "hkd", + "huf", + "idr", + "ils", + "inr", + "jpy", + "krw", + "kwd", + "lkr", + "mmk", + "mxn", + "myr", + "ngn", + "nok", + "nzd", + "php", + "pkr", + "pln", + "rub", + "sar", + "sek", + "sgd", + "thb", + "try", + "twd", + "uah", + "vef", + "vnd", + "zar", + "xdr", + "xag", + "xau", + "bits", + "sats", +] +"""List of the supported currencies by CoinGecko API.""" + + +def __is_currency_supported(currency: str) -> bool: + """ + Return whether or nothe the currency is supported by CoinGecko API + + Parameters + ---------- + currency : str + The VS currency, for example eur, usd, btc or eth. + + Returns + ------- + bool + a boolean value containing True if the currency is supported or False if it not supported. + """ + + return currency in SUPPORTED_CURRENCIES + + +def get_grin_price(currency: str) -> dict: + """ + Get the price of Grin using the CoinGecko API + + Parameters + ---------- + currency : str + The VS currency, for example eur, usd, btc or eth. + + Raises + ------ + Exception + If the currency uses is not supported. + + Returns + ------ + dict + A dictionary with: price, market cap, 24h change and 24h volume. + """ + + if not __is_currency_supported(currency=currency): + raise Exception(f"{currency} is not a supported currency by CoinGecko API") + + result: dict = requests.get( + url=f"https://api.coingecko.com/api/v3/simple/price?ids=grin&vs_currencies={currency}&include_market_cap=true&include_24hr_vol=true&include_24hr_change=true&include_last_updated_at=true&precision=true" + ).json() + + return { + "price": result["grin"][currency], + "market_cap": result["grin"][currency + "_market_cap"], + "24h_change": result["grin"][currency + "_24h_change"], + "24h_vol": result["grin"][currency + "_24h_vol"], + } diff --git a/src/modules/utils/grinchck.py b/src/modules/utils/grinchck.py new file mode 100644 index 0000000..8b044b0 --- /dev/null +++ b/src/modules/utils/grinchck.py @@ -0,0 +1,37 @@ +""" +Mimblewimble transactions are interactive, meaning both parties need some kind of communication to interact with each other and exchange the necessary data to create a transaction. Slatepack addresses are also used to derive a Tor address. By default, the sender's wallet will try to communicate with the receiver's wallet via Tor. However, if the Tor connection between the wallets is not successful for whatever reason, grin defaults to manually exchanging slate text messages, also called slatepacks. manually. + +This module uses the Grin Address Checker API to check whether or not an address is accessible through Tor. +""" +from requests import Response, post +from requests.exceptions import ConnectionError + + +def connect(slatepack_address: str, api_url: str) -> bool: + """ + Attempts to connect to a wallet through Tor. This will return True if the wallet is reached. + + Parameters + ---------- + slatepack_address : str + Slatepack address if the wallet. + api_url : str + URL of grinchck API. + + Raises + ------ + ConnectionError + If the API is not available. + + Returns + ------ + bool + True when the wallet is reachable, otherwise False. + """ + try: + data: dict = {"wallet": slatepack_address} + result: Response = post(url=api_url, data=data) + return result.status_code == 200 + except ConnectionError: + error = "Unable to connect to the GrinChck API" + raise Exception(error) diff --git a/src/modules/utils/kdf.py b/src/modules/utils/kdf.py new file mode 100644 index 0000000..de2be69 --- /dev/null +++ b/src/modules/utils/kdf.py @@ -0,0 +1,76 @@ +""" +Key derivation functions derive bytes suitable for cryptographic operations from passwords or other data sources using a pseudo-random function (PRF). +""" +import base64 +import os +from pathlib import Path + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC + + +class __SaltManager(object): + """Salt Manager internal class.""" + + def __init__(self): + self.path = str(Path.home() / ".salt") + + def get(self): + if not os.path.exists(self.path): + return self._generate_and_store() + return self._read() + + def _generate_and_store(self): + salt = os.urandom(16) + with open(self.path, "xb") as f: + f.write(salt) + return salt + + def _read(self): + with open(self.path, "rb+") as f: + return f.read() + + +def derive_key(passphrase) -> bytes: + """ + Derive a key using a passphrase. The method will try to get an already stored `salt` or generate a new one if necessary. + + Parameters + ---------- + passphrase : str + Passphrase or password. + + Returns + ------- + bytes + Raw bytes of the derived key. + """ + salt = __SaltManager() + + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=salt.get(), + iterations=1000, + backend=default_backend(), + ) + + return kdf.derive(passphrase) + + +def encode_key(key: bytes): + """ + Encode bytes using the URL- and filesystem-safe Base64 alphabet. Argument *derived_key* is a bytes-like object to encode. The result is returned as a bytes object. + + Parameters + ---------- + key : bytes + Bytes of the key to encode. + + Returns + ------- + bytes + Raw bytes. + """ + return base64.urlsafe_b64encode(key) diff --git a/src/modules/utils/processes.py b/src/modules/utils/processes.py new file mode 100644 index 0000000..bf6d4b5 --- /dev/null +++ b/src/modules/utils/processes.py @@ -0,0 +1,93 @@ +""" +Simple actions such as finding and killing processes are useful for troubleshooting. +""" +from __future__ import annotations + +import os +import signal +from collections.abc import Callable + +import psutil + + +def find(name: str) -> list[psutil.Process]: + """ + Return a list of processes matching with 'name'. + + Parameters + ---------- + name : str + Name of the process. + + Returns + ------- + bool + A list of `psutil.Process`. + """ + name = name.casefold() + result: list[psutil.Process] = [] + processes = {process.pid: process for process in psutil.process_iter()} + for process in processes.values(): + if not process.is_running(): + continue + try: + if process.name().casefold() == name: + result.append(process) + elif os.path.basename(process.exe()).casefold() == name: + result.append(process) + elif process.cmdline() and process.cmdline()[0].casefold() == name: + result.append(process) + except psutil.AccessDenied: + continue + except psutil.NoSuchProcess: + continue + + return result + + +def kill( + pid: int, + sig: signal.Signals = signal.SIGTERM, + include_parent: bool = True, + timeout: float | None = None, + on_terminate: Callable[[psutil.Process], object] | None = None, +) -> tuple[list[psutil.Process], list[psutil.Process]]: + """ + Kill a process tree (including grandchildren) with signal + "sig" and return a (gone, still_alive) tuple. + "on_terminate", if specified, is a callback function which is + called as soon as a child terminates. + + Return a (gone, alive) tuple indicating which processes + are gone and which ones are still alive. + + Parameters + ---------- + pid : int + PID of the process. + sig : signal.Signals + SIGTERM by default. + include_parent : bool + Kill also the parent processs. + timeout : float | None + Function will return as soon as all processes terminate or when *timeout* occurs. + on_terminate : Callable[[psutil.Process], object] | None + *callback* or function which gets called every time a process terminates. + + Returns + ------- + bool + A `(gone, alive)` tuple indicating which processes are gone and which ones are still alive. + """ + assert pid != os.getpid(), "I won't kill myself!" + parent = psutil.Process(pid) + children = parent.children(recursive=True) + if include_parent: + children.append(parent) + for p in children: + try: + p.send_signal(sig) + except psutil.NoSuchProcess: + pass + gone, alive = psutil.wait_procs(children, timeout=timeout, callback=on_terminate) + return (gone, alive) diff --git a/src/modules/wallet/__init__.py b/src/modules/wallet/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/wallet/session.py b/src/modules/wallet/session.py new file mode 100644 index 0000000..f07139f --- /dev/null +++ b/src/modules/wallet/session.py @@ -0,0 +1,82 @@ +""" +After opening a wallet, a session token is needed to communicate with the Grin API. The token is used to identify the wallet. This module contains what is needed to store the token information. +""" +import os +from pathlib import Path + +from cryptography.fernet import Fernet + +from modules.utils.kdf import derive_key, encode_key + + +def store( + wallet: str, + token: str, + password: str, +) -> bool: + """ + Saves the session token locally. The token is encrypted before saving. + + Parameters + ---------- + wallet : str + Walle name. This is uses to name the file like: `[wallet].token`. + token : str + Session token used to communicate with the Grin API. + password: str + Password used to encrypt the session token. + + Returns + ------ + bool + It would be True if the file is correctly stored. + """ + + passphrase: str = f"{wallet}{password}" + key: bytes = derive_key(passphrase=passphrase.encode()) + encoded_key = encode_key(key) + + token_path = Path.home().joinpath( + os.getenv("GRINPP_CLI_DATA_PATH", ".grinplusplus"), f"{wallet}.token" + ) + with open(token_path, "wb+") as file: + file.write(Fernet(encoded_key).encrypt(token.encode())) + + return token_path.exists() + + +def token(wallet: str, password: str) -> str: + """ + Returns the corresponding session token of an opened wallet. + + Parameters + ---------- + wallet : str + Walle name. This is uses to name the file like: `[wallet].token`. + password: str + Password used to encrypt the session token. + + Returns + ------ + str + A string containing the session token. + """ + session_token: str = "" + encrypted_session_token: bytes = bytes() + + passphrase: str = f"{wallet}{password}" + key: bytes = derive_key(passphrase.encode()) + encoded_key: bytes = encode_key(key) + + token_path: Path = Path.home().joinpath( + os.getenv("GRINPP_CLI_DATA_PATH", ".grinplusplus"), f"{wallet}.token" + ) + with open(token_path, "rb") as file: + encrypted_session_token = file.read() + + try: + session_token = Fernet(encoded_key).decrypt(encrypted_session_token).decode() + except: + raise Exception("Invalid password.") + + return session_token