From 617f78f3807c64541a06614ccac2cd57920a37b9 Mon Sep 17 00:00:00 2001 From: jba123 <9620lim@gmail.com> Date: Mon, 29 Oct 2018 05:27:32 +0000 Subject: [PATCH] evm-lite-client --- .gitignore | 3 + LICENSE | 21 ++ Makefile | 24 ++ README.md | 312 +++++++++++++++++++ cli/.gitignore | 4 + cli/CHANGELOG.md | 9 + cli/index.js | 0 cli/package.json | 31 ++ cli/src/classes/Config.js | 81 +++++ cli/src/classes/Config.ts | 97 ++++++ cli/src/classes/DataDirectory.js | 30 ++ cli/src/classes/DataDirectory.ts | 39 +++ cli/src/classes/Database.js | 23 ++ cli/src/classes/Database.ts | 34 ++ cli/src/classes/Keystore.js | 70 +++++ cli/src/classes/Keystore.ts | 82 +++++ cli/src/classes/Log.js | 37 +++ cli/src/classes/Log.ts | 46 +++ cli/src/classes/Session.js | 51 +++ cli/src/classes/Session.ts | 74 +++++ cli/src/classes/Transactions.js | 30 ++ cli/src/classes/Transactions.ts | 35 +++ cli/src/commands/AccountsCreate.js | 53 ++++ cli/src/commands/AccountsCreate.ts | 51 +++ cli/src/commands/AccountsGet.js | 65 ++++ cli/src/commands/AccountsGet.ts | 67 ++++ cli/src/commands/AccountsList.js | 59 ++++ cli/src/commands/AccountsList.ts | 60 ++++ cli/src/commands/ConfigSet.js | 120 +++++++ cli/src/commands/ConfigSet.ts | 126 ++++++++ cli/src/commands/ConfigView.js | 22 ++ cli/src/commands/ConfigView.ts | 26 ++ cli/src/commands/Info.js | 49 +++ cli/src/commands/Info.ts | 46 +++ cli/src/commands/Interactive.js | 12 + cli/src/commands/Interactive.ts | 16 + cli/src/commands/LogsClear.js | 23 ++ cli/src/commands/LogsClear.ts | 24 ++ cli/src/commands/LogsView.js | 37 +++ cli/src/commands/LogsView.ts | 37 +++ cli/src/commands/Test.js | 23 ++ cli/src/commands/Test.ts | 17 + cli/src/commands/TransactionsGet.js | 71 +++++ cli/src/commands/TransactionsGet.ts | 76 +++++ cli/src/commands/TransactionsList.js | 66 ++++ cli/src/commands/TransactionsList.ts | 72 +++++ cli/src/commands/Transfer.js | 110 +++++++ cli/src/commands/Transfer.ts | 114 +++++++ cli/src/evmlc.js | 85 +++++ cli/src/evmlc.ts | 97 ++++++ cli/src/utils/Globals.js | 41 +++ cli/src/utils/Globals.ts | 83 +++++ cli/tsconfig.json | 13 + lib/.gitignore | 4 + lib/.npmignore | 7 + lib/@types/index.d.ts | 447 +++++++++++++++++++++++++++ lib/@types/index.test-d..ts | 0 lib/CHANGELOG.md | 27 ++ lib/dist/Controller.js | 169 ++++++++++ lib/dist/evm/Account.js | 54 ++++ lib/dist/evm/Client.js | 102 ++++++ lib/dist/evm/SolidityContract.js | 188 +++++++++++ lib/dist/evm/SolidityFunction.js | 89 ++++++ lib/dist/evm/Transaction.js | 174 +++++++++++ lib/dist/evm/Wallet.js | 19 ++ lib/dist/evm/utils/Interfaces.js | 7 + lib/dist/evm/utils/checks.js | 34 ++ lib/dist/evm/utils/errors.js | 23 ++ lib/index.js | 2 + lib/package.json | 30 ++ lib/src/Controller.ts | 192 ++++++++++++ lib/src/evm/Account.ts | 100 ++++++ lib/src/evm/Client.ts | 114 +++++++ lib/src/evm/SolidityContract.ts | 212 +++++++++++++ lib/src/evm/SolidityFunction.ts | 113 +++++++ lib/src/evm/Transaction.ts | 188 +++++++++++ lib/src/evm/Wallet.ts | 27 ++ lib/src/evm/utils/Interfaces.ts | 69 +++++ lib/src/evm/utils/checks.ts | 35 +++ lib/src/evm/utils/errors.ts | 21 ++ lib/tests/Controller.test.js | 0 lib/tests/Controller.test.ts | 0 lib/tsconfig.json | 17 + package.json | 8 + 84 files changed, 5366 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 cli/.gitignore create mode 100644 cli/CHANGELOG.md create mode 100644 cli/index.js create mode 100644 cli/package.json create mode 100644 cli/src/classes/Config.js create mode 100644 cli/src/classes/Config.ts create mode 100644 cli/src/classes/DataDirectory.js create mode 100644 cli/src/classes/DataDirectory.ts create mode 100644 cli/src/classes/Database.js create mode 100644 cli/src/classes/Database.ts create mode 100644 cli/src/classes/Keystore.js create mode 100644 cli/src/classes/Keystore.ts create mode 100644 cli/src/classes/Log.js create mode 100644 cli/src/classes/Log.ts create mode 100644 cli/src/classes/Session.js create mode 100644 cli/src/classes/Session.ts create mode 100644 cli/src/classes/Transactions.js create mode 100644 cli/src/classes/Transactions.ts create mode 100644 cli/src/commands/AccountsCreate.js create mode 100644 cli/src/commands/AccountsCreate.ts create mode 100644 cli/src/commands/AccountsGet.js create mode 100644 cli/src/commands/AccountsGet.ts create mode 100644 cli/src/commands/AccountsList.js create mode 100644 cli/src/commands/AccountsList.ts create mode 100644 cli/src/commands/ConfigSet.js create mode 100644 cli/src/commands/ConfigSet.ts create mode 100644 cli/src/commands/ConfigView.js create mode 100644 cli/src/commands/ConfigView.ts create mode 100644 cli/src/commands/Info.js create mode 100644 cli/src/commands/Info.ts create mode 100644 cli/src/commands/Interactive.js create mode 100644 cli/src/commands/Interactive.ts create mode 100644 cli/src/commands/LogsClear.js create mode 100644 cli/src/commands/LogsClear.ts create mode 100644 cli/src/commands/LogsView.js create mode 100644 cli/src/commands/LogsView.ts create mode 100644 cli/src/commands/Test.js create mode 100644 cli/src/commands/Test.ts create mode 100644 cli/src/commands/TransactionsGet.js create mode 100644 cli/src/commands/TransactionsGet.ts create mode 100644 cli/src/commands/TransactionsList.js create mode 100644 cli/src/commands/TransactionsList.ts create mode 100644 cli/src/commands/Transfer.js create mode 100644 cli/src/commands/Transfer.ts create mode 100755 cli/src/evmlc.js create mode 100644 cli/src/evmlc.ts create mode 100644 cli/src/utils/Globals.js create mode 100644 cli/src/utils/Globals.ts create mode 100644 cli/tsconfig.json create mode 100644 lib/.gitignore create mode 100644 lib/.npmignore create mode 100644 lib/@types/index.d.ts create mode 100644 lib/@types/index.test-d..ts create mode 100644 lib/CHANGELOG.md create mode 100644 lib/dist/Controller.js create mode 100644 lib/dist/evm/Account.js create mode 100644 lib/dist/evm/Client.js create mode 100644 lib/dist/evm/SolidityContract.js create mode 100644 lib/dist/evm/SolidityFunction.js create mode 100644 lib/dist/evm/Transaction.js create mode 100644 lib/dist/evm/Wallet.js create mode 100644 lib/dist/evm/utils/Interfaces.js create mode 100644 lib/dist/evm/utils/checks.js create mode 100644 lib/dist/evm/utils/errors.js create mode 100644 lib/index.js create mode 100644 lib/package.json create mode 100644 lib/src/Controller.ts create mode 100644 lib/src/evm/Account.ts create mode 100644 lib/src/evm/Client.ts create mode 100644 lib/src/evm/SolidityContract.ts create mode 100644 lib/src/evm/SolidityFunction.ts create mode 100644 lib/src/evm/Transaction.ts create mode 100644 lib/src/evm/Wallet.ts create mode 100644 lib/src/evm/utils/Interfaces.ts create mode 100644 lib/src/evm/utils/checks.ts create mode 100644 lib/src/evm/utils/errors.ts create mode 100644 lib/tests/Controller.test.js create mode 100644 lib/tests/Controller.test.ts create mode 100644 lib/tsconfig.json create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f556c92 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +.vscode +package-lock.json diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ab73971 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Mosaic Networks + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ad954eb --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +up: build link +clean: clean-cli clean-lib unlink + +build: build-lib build-cli +link: + bash -c "cd cli && npm link && cd .." +unlink: + bash -c "cd cli && npm unlink && cd .." + +# Build CLI dependencies +build-cli: + bash -c "npm run build:cli" + +# Build Client dependencies +build-lib: + bash -c "npm run build:lib" + +clean-lib: + bash -c "npm run clean:lib" + +clean-cli: + bash -c "npm run clean:cli" + +.PHONY: up link clean clean-lib clean-cli build build-cli build-lib \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..206fdd5 --- /dev/null +++ b/README.md @@ -0,0 +1,312 @@ +# EVM-Lite Client (Javascript) +LondonCoin is building the improved core utilizing "Push-Pull" algorithm. +Original code provided by Mosaic Networks. + +A Javascript client library and Command Line Interface to interact with +EVM-Lite. + +## Installation + +To begin with, you will need to install Node and NPM, which are bundled together +in the installation package from the [Node website](https://nodejs.org/en/). + +This project was built with Node version 10.10.0 and NPM version 6.1.0. + +### Makefile + +To download Javascript dependencies and install `evmlc`, run: + +``` +$ make +``` + +To clean all of the steps from the `make` command run: + +``` +$ make clean +``` + +This removes all dependencies for Client and CLI and also removes the symlink. + +If the `make` was successful you should now be able to run `evmlc`: + +```console +$ evmlc + + A Command Line Interface to interact with EVM-Lite. + + Current Data Directory: [...]/.evmlc + + Commands: + + help [command...] Provides help for a given command. + exit Exits application. + config view Output current configuration file as JSON. + config set [options] Set values of the configuration inside the data directory. + accounts create [options] Allows you to create and encrypt accounts locally. Created accounts will either be placed in the keystore folder inside the data directory provided by the global --datadir, -d flag or if no flag is provided, in the keystore + specified in the configuration file. + accounts list [options] List all accounts in the local keystore directory provided by the configuration file. This command will also get a balance and nonce for all the accounts from the node if a valid connection is established. + accounts get [options] [address] Gets account balance and nonce from a node with a valid connection. + interactive Enter into interactive mode with data directory provided by --datadir, -d or default. + transfer [options] Initiate a transfer of token(s) to an address. Default values for gas and gas prices are set in the configuration file. + info [options] Testing purposes. +``` + +## Configuration + +The first time it runs, and if no options are specified, `evmlc` creates a +special directory in a default location (`~/.evmlc` on Linux and Mac), where it +stores any relevant information. In particular, this directory contains the +following items: + + - **config.toml**: where global options are specified. These values may be + overwritten by CLI flags. + - **keystore**: where all encrypted account keys are stored. + - **pwd.txt**: password file to decrypt keys. + +Example config.toml: + ```toml +[connection] +host = "127.0.0.1" +port = "8080" + +[defaults] +from = "" +gas = 100000.0 +gasPrice = 0.0 + +[storage] +keystore = "/home/user/.evmlc/keystore" +password = "/home/user/.evmlc/pwd.txt" + ``` + +The easiest way to manage configuration is through the `config` command in +interactive mode. + +```console +$ evmlc i +Entered interactive mode with configuration file: [...]/.evmlc/config.toml +evmlc$ + +``` +To change default configuration values run `config set` or `c s`. You will be +taken to an interactive prompt to change connection and default values. + +```console +evmlc$ config set + +? Host: 127.0.0.1 +? Port: 8080 +? Default From Address: +? Default Gas: 0 +? Default Gas Price: 0 +``` + +## Commands +By default all commands will output raw JSON unless the `-f, --formatted` flag +is provided. A connection to the node is not required unless stated in each +command. + +The global flag `-d, --datadir` specifies the directory where `keystore`, +`pwd.txt` and `config.toml` are stored unless overwritten by specific flags. +Note that if this flag is not provided, it will default to `~/.evmlc`. + +## Getting Started + +We explain how to use `evmlc` against a single `evm-lite` node. We will walk +through creating accounts, making transfers, and viewing account information. + +### 1) Run `evmlc` in interactive mode + +```bash +user:~$ evmlc i +Entered interactive mode with data directory: /home/user/.evmlc +evmlc$ +``` + +### 2) Create an account + +While still in interactive mode, type the command `accounts create` and select +the default options in the prompt: + +```bash +evmlc$ accounts create +? Enter keystore output path: /home/user/.evmlc/keystore +? Enter password file path: /home/user/.evmlc/pwd.txt +{"version":3,"id":"f62fe161-0870-4553-9cf2-3155b19d4b59","address":"477f22b53038b745bb039653b91bdaa88c8bf94d","crypto":{"ciphertext":"XXX","cipherparams":{"iv":"26807046432c098a51a563393dcd91fa"},"cipher":"aes-128-ctr","kdf":"scrypt","kdfparams":{"dklen":32,"salt":"XXX","n":8192,"r":8,"p":1},"mac":"XXX"}} +evmlc$ +``` + +#### What happened? + +It created an account with address `477f22b53038b745bb039653b91bdaa88c8bf94d`, +and added the corresponding keyfile, password protected with the password file, +in the keystore directory + +#### What is an account? + +EVM-Lite uses the same account model as Ethereum. Accounts represent identities +of external agents, and are associated with a balance (and storage for Contract +accounts). They rely on public key cryptography to sign transactions so that the +EVM can securely validate the identity of a transaction sender. + +Using the same account model as Ethereum doesn't mean that existing Ethereum +accounts automatically have the same balance in EVM-Lite (or vice versa). In +Ethereum, balances are denoted in Ether, the cryptocurrency maintained by the +public Ethereum network. On the other hand, every EVM-Lite network (even a +single node network) maintains a completely separate ledger, and may use any +name for the corresponding coin. + +What follows is mostly taken from the [Ethereum Docs](http://ethdocs.org/en/latest/account-management.html): + +Accounts are objects in the EVM-Lite State. They come in two types: Externally +owned accounts, and Contract accounts. Externally owned accounts have a balance, +and Contract accounts have a balance and storage. The EVM-Lite State is the +state of all accounts which is updated with every transaction. The underlying +consensus engine ensures that every participant in an EVM-Lite network processes +the same transactions in the same order, thereby arriving at the same State. + +Restricting EVM-Lite to externally owned accounts makes for an “altcoin” system +that can only be used to transfer coins. The use of Contract accounts with the +EVM make it possible to deploy and use *Smart Contracts* which we will explore +in another document. + +#### What is an account file? + +This is best explained in the +[Ethereum Docs](http://ethdocs.org/en/latest/account-management.html): + +>Every account is defined by a pair of keys, a private key and public key. +>Accounts are indexed by their address which is derived from the public key by +>taking the last 20 bytes. Every private key/address pair is encoded in a +>keyfile. Keyfiles are JSON text files which you can open and view in any text +>editor. The critical component of the keyfile, your account’s private key, is +>always encrypted, and it is encrypted with the password you enter when you +>create the account. + +### 3) Start an `evm-lite` node and pre-allocate funds to our address + +If you haven't done so yet, please install and familiarize yourself with +[EVM-Lite](https://github.com/mosaicnetworks/evm-lite), our lightweight Ethereum +node with interchangeable consensus. + +In a separate terminal from the interactive `evmlc` session, start a single node +(in Solo mode) and specify the previously created account address as the genesis +account: + +```bash +user:~$ evml solo --genesis 477f22b53038b745bb039653b91bdaa88c8bf94d +DEBU[0000] Config Base="{/home/user/.evm-lite debug}" Eth="&{/home/user/.evm-lite/eth/genesis.json /home/user/.evm-lite/eth/keystore /home/user/.evm-lite/eth/pwd.txt /home/user/.evm-lite/eth/chaindata :8080 128}" +DEBU[0000] Config Eth="&{/home/user/.evm-lite/eth/genesis.json /home/user/.evm-lite/eth/keystore /home/user/.evm-lite/eth/pwd.txt /home/user/.evm-lite/eth/chaindata :8080 128}" genesis=477f22b53038b745bb039653b91bdaa88c8bf94d +DEBU[0000] Writing genesis file +DEBU[0000] INIT module=solo +DEBU[0000] Adding account address=477f22b53038b745bb039653b91bdaa88c8bf94d +DEBU[0000] Committed root=0x1aa38473e2f6fc5ada1bb0e6eeddc1fdeda991ff7a50150e16306e018d9a7639 +DEBU[0000] Reset WAS +DEBU[0000] Reset TxPool +INFO[0000] serving api... + +``` + +This booted the node and assigned a lot of coins to our account. By default, +`evm-lite` is configured to listen on any interface on port 8080 (:8080), and +`evmlc` is configured to connect to `localhost:8080`, so the client and node are +ready to talk. + +How many coins where assigned to the account? let's check! + +### 4) List accounts + +Back in the interactive `evmlc` session, type `accounts list -f` + +```bash +evmlc$ accounts list -f +.----------------------------------------------------------------------------------------. +| # | Address | Balance | Nonce | +|---|--------------------------------------------|-------------------------------|-------| +| 1 | 0x477F22b53038b745BB039653b91bdaA88c8bF94d | 1,337,000,000,000,000,000,000 | 0 | +'----------------------------------------------------------------------------------------' +evmlc$ +``` + +The command went through the accounts in the keystore, connected to the node to +retrieve the corresponding balance, and displayed it nicely on the screen. + + +### 5) Create another account + +```bash +evmlc$ accounts create +? Enter keystore output path: /home/user/.evmlc/keystore +? Enter password file path: /home/user/.evmlc/pwd.txt +{"version":3,"id":"1cd4f6fc-5d66-49b9-b3b2-f0ba0798450c","address":"988456018729c15a6914a2c5ba1a753f76ec36dc","crypto":{"ciphertext":"XXX","cipherparams":{"iv":"421d86663e8cd0915ab0bbedb0e14d96"},"cipher":"aes-128-ctr","kdf":"scrypt","kdfparams":{"dklen":32,"salt":"XXX","n":8192,"r":8,"p":1},"mac":"XXX"}} +evmlc$ +``` + +This one has the address `988456018729c15a6914a2c5ba1a753f76ec36dc` + +### 6) Transfer coins from one account to another + +Type `transfer` and follow the instructions to transfer coins from the first +account to the second account. + +```bash +evmlc$ transfer +? From: 0x477F22b53038b745BB039653b91bdaA88c8bF94d +? To 988456018729c15a6914a2c5ba1a753f76ec36dc +? Value: 100 +? Gas: 25000 +? Gas Price: 0 +{"txHash":"0xa64b35b2228f00d9b5ba01fcd4c8bcd1c89b33d8b5fd917ea2c4d4de2a7d43ea"} +Transaction submitted. +evmlc$ +``` + +#### What happened? + +It **created a transaction** to send 100 coins from the first account to the +second account, **signed it** with the sender's private key, and **sent it** to +the evm-lite node. The node responded with the transaction hash, which +identifies our transaction in EVM-Lite, and allows us to query its results. + +#### What is a transaction? + +A transaction is a signed data package that contains instructions for the EVM. +It can contain instructions to move coins from one account to another, create a +new Contract account, or call an existing Contract account. Transactions are +encoded using the custom Ethereum scheme, RLP, and contain the following fields: + +- the recipient of the message, +- a signature identifying the sender and proving their intention to send the +transaction. +- The number of coin to transfer from the sender to the recipient, +- an optional data field, which can contain the message sent to a contract, +- a STARTGAS value, representing the maximum number of computational steps the +transaction execution is allowed to take, +- a GASPRICE value, representing the fee the sender is willing to pay for gas. +One unit of gas corresponds to the execution of one atomic instruction, i.e., a +computational step. + +### 7) Check accounts again + +```bash +evmlc$ accounts list -f +.----------------------------------------------------------------------------------------. +| # | Address | Balance | Nonce | +|---|--------------------------------------------|-------------------------------|-------| +| 1 | 0x477F22b53038b745BB039653b91bdaA88c8bF94d | 1,336,999,999,999,999,999,900 | 1 | +| 2 | 0x988456018729C15A6914A2c5bA1A753F76eC36Dc | 100 | 0 | +'----------------------------------------------------------------------------------------' +evmlc$ +``` + +### Conclusion + +We showed how to use `evmlc` to create an EVM-Lite account and transfer coins +from one account to another. We used a single EVM-Lite node, running in Solo +mode, for the purpose of demonstration, but the same concepts apply with +networks consisting of multiple nodes, powered by other consensus algorithms +(like LondonCoin , Babble or Raft). In another document, we will describe how to create, +publish, and interact with SmartContracts. + +Many more features to come... diff --git a/cli/.gitignore b/cli/.gitignore new file mode 100644 index 0000000..c99b0f9 --- /dev/null +++ b/cli/.gitignore @@ -0,0 +1,4 @@ +node_modules +package-lock.json +PROGRESS.txt +.idea diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md new file mode 100644 index 0000000..1285d48 --- /dev/null +++ b/cli/CHANGELOG.md @@ -0,0 +1,9 @@ +## UNRELEASED + +FEATURES: + +IMPROVEMENTS: + +SECURITY: + +BUG FIXES: \ No newline at end of file diff --git a/cli/index.js b/cli/index.js new file mode 100644 index 0000000..e69de29 diff --git a/cli/package.json b/cli/package.json new file mode 100644 index 0000000..308e953 --- /dev/null +++ b/cli/package.json @@ -0,0 +1,31 @@ +{ + "name": "cli", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "ascii-table": "0.0.9", + "chalk": "^2.4.1", + "figlet": "^1.2.0", + "inquirer": "latest", + "json-bigint": "^0.3.0", + "mkdirp": "^0.5.1", + "toml": "^2.3.3", + "tomlify-j0.4": "^3.0.0", + "vorpal": "^1.12.0" + }, + "bin": { + "evmlc": "./src/evmlc.js" + }, + "devDependencies": { + "@types/chalk": "^2.2.0", + "@types/node": "^10.11.0", + "@types/vorpal": "^1.11.0" + } +} diff --git a/cli/src/classes/Config.js b/cli/src/classes/Config.js new file mode 100644 index 0000000..8dfba08 --- /dev/null +++ b/cli/src/classes/Config.js @@ -0,0 +1,81 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const fs = require("fs"); +const toml = require("toml"); +const tomlify = require("tomlify-j0.4"); +const mkdir = require("mkdirp"); +const Globals_1 = require("../utils/Globals"); +const path = require("path"); +const Keystore_1 = require("./Keystore"); +const DataDirectory_1 = require("./DataDirectory"); +class Config { + constructor(datadir, filename) { + this.datadir = datadir; + this.filename = filename; + this.data = Config.default(this.datadir); + this._initialData = Config.default(this.datadir); + this.path = path.join(datadir, filename); + if (fs.existsSync(this.path)) { + let tomlData = Config.readFile(this.path); + this.data = toml.parse(tomlData); + this._initialData = toml.parse(tomlData); + } + } + static readFile(path) { + if (fs.existsSync(path)) { + return fs.readFileSync(path, 'utf8'); + } + } + static default(datadir) { + return { + connection: { + host: '127.0.0.1', + port: '8080' + }, + defaults: { + from: '', + gas: 100000, + gasPrice: 0 + }, + storage: { + keystore: path.join(datadir, 'keystore'), + password: path.join(datadir, 'pwd.txt'), + } + }; + } + static defaultTOML(datadir) { + return tomlify.toToml(Config.default(datadir), { spaces: 2 }); + } + toTOML() { + return tomlify.toToml(this.data, { spaces: 2 }); + } + save() { + Globals_1.default.info(`Config is being read from and updated at ${this.path}`); + if (Globals_1.default.isEquivalentObjects(this.data, this._initialData)) { + Globals_1.default.warning('No changes in configuration detected.'); + return false; + } + else { + let list = this.path.split('/'); + list.pop(); + let configFileDir = list.join('/'); + if (!fs.existsSync(configFileDir)) { + mkdir.mkdirp(configFileDir); + } + fs.writeFileSync(this.path, this.toTOML()); + this._initialData = toml.parse(this.toTOML()); + Globals_1.default.success('Configuration file updated.'); + return true; + } + } + getOrCreateKeystore(password) { + DataDirectory_1.default.createDirectoryIfNotExists(this.data.storage.keystore); + return new Keystore_1.default(this.data.storage.keystore, password); + } + getOrCreatePasswordFile() { + let password = 'supersecurepassword'; + DataDirectory_1.default.createOrReadFile(this.data.storage.password, password); + return this.data.storage.password; + } +} +exports.default = Config; diff --git a/cli/src/classes/Config.ts b/cli/src/classes/Config.ts new file mode 100644 index 0000000..8f33b27 --- /dev/null +++ b/cli/src/classes/Config.ts @@ -0,0 +1,97 @@ +import * as fs from 'fs'; +import * as toml from "toml"; +import * as tomlify from 'tomlify-j0.4'; +import * as mkdir from 'mkdirp'; + +import Globals from "../utils/Globals"; +import * as path from "path"; +import Keystore from "./Keystore"; +import DataDirectory from "./DataDirectory"; + +export default class Config { + + public data: any; + public path: string; + private _initialData: any; + + constructor(public datadir: string, public filename: string) { + this.data = Config.default(this.datadir); + this._initialData = Config.default(this.datadir); + + this.path = path.join(datadir, filename); + + if (fs.existsSync(this.path)) { + let tomlData: string = Config.readFile(this.path); + + this.data = toml.parse(tomlData); + this._initialData = toml.parse(tomlData); + } + } + + static readFile(path: string): string { + if (fs.existsSync(path)) { + return fs.readFileSync(path, 'utf8'); + } + } + + static default(datadir: string) { + return { + connection: { + host: '127.0.0.1', + port: '8080' + }, + defaults: { + from: '', + gas: 100000, + gasPrice: 0 + }, + storage: { + keystore: path.join(datadir, 'keystore'), + password: path.join(datadir, 'pwd.txt'), + } + } + } + + static defaultTOML(datadir: string) { + return tomlify.toToml(Config.default(datadir), {spaces: 2}); + } + + toTOML(): string { + return tomlify.toToml(this.data, {spaces: 2}) + } + + save(): boolean { + Globals.info(`Config is being read from and updated at ${this.path}`); + if (Globals.isEquivalentObjects(this.data, this._initialData)) { + Globals.warning('No changes in configuration detected.'); + return false; + } else { + let list = this.path.split('/'); + list.pop(); + + let configFileDir = list.join('/'); + + if (!fs.existsSync(configFileDir)) { + mkdir.mkdirp(configFileDir); + } + + fs.writeFileSync(this.path, this.toTOML()); + this._initialData = toml.parse(this.toTOML()); + Globals.success('Configuration file updated.'); + + return true; + } + } + + getOrCreateKeystore(password: string): Keystore { + DataDirectory.createDirectoryIfNotExists(this.data.storage.keystore); + return new Keystore(this.data.storage.keystore, password); + } + + getOrCreatePasswordFile(): string { + let password: string = 'supersecurepassword'; + DataDirectory.createOrReadFile(this.data.storage.password, password); + return this.data.storage.password; + } + +} \ No newline at end of file diff --git a/cli/src/classes/DataDirectory.js b/cli/src/classes/DataDirectory.js new file mode 100644 index 0000000..ccdbaf8 --- /dev/null +++ b/cli/src/classes/DataDirectory.js @@ -0,0 +1,30 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const fs = require("fs"); +const mkdir = require("mkdirp"); +const path = require("path"); +const Config_1 = require("./Config"); +class DataDirectory { + constructor(path) { + this.path = path; + DataDirectory.createDirectoryIfNotExists(path); + } + static createDirectoryIfNotExists(path) { + if (!fs.existsSync(path)) { + mkdir.sync(path); + } + } + static createOrReadFile(path, data) { + if (!fs.existsSync(path)) { + fs.writeFileSync(path, data); + return data; + } + return fs.readFileSync(path, 'utf8'); + } + createAndGetConfig() { + let configFilePath = path.join(this.path, 'config.toml'); + DataDirectory.createOrReadFile(configFilePath, Config_1.default.defaultTOML(this.path)); + return new Config_1.default(this.path, 'config.toml'); + } +} +exports.default = DataDirectory; diff --git a/cli/src/classes/DataDirectory.ts b/cli/src/classes/DataDirectory.ts new file mode 100644 index 0000000..efc1fc7 --- /dev/null +++ b/cli/src/classes/DataDirectory.ts @@ -0,0 +1,39 @@ +import * as fs from "fs"; +import * as mkdir from "mkdirp"; +import * as path from 'path'; + +import Config from "./Config"; +import Keystore from "./Keystore"; + + +export default class DataDirectory { + + constructor(readonly path: string) { + DataDirectory.createDirectoryIfNotExists(path); + } + + static createDirectoryIfNotExists(path: string): void { + if (!fs.existsSync(path)) { + mkdir.sync(path); + } + } + + static createOrReadFile(path: string, data: string): string { + if (!fs.existsSync(path)) { + fs.writeFileSync(path, data); + + return data; + } + + return fs.readFileSync(path, 'utf8'); + } + + createAndGetConfig(): Config { + let configFilePath = path.join(this.path, 'config.toml'); + + DataDirectory.createOrReadFile(configFilePath, Config.defaultTOML(this.path)); + return new Config(this.path, 'config.toml'); + } + + +} \ No newline at end of file diff --git a/cli/src/classes/Database.js b/cli/src/classes/Database.js new file mode 100644 index 0000000..093bcdf --- /dev/null +++ b/cli/src/classes/Database.js @@ -0,0 +1,23 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const JSONBig = require("json-bigint"); +const fs = require("fs"); +const Transactions_1 = require("./Transactions"); +const DataDirectory_1 = require("./DataDirectory"); +class Database { + constructor(path) { + this.path = path; + this._data = JSONBig.parse(DataDirectory_1.default.createOrReadFile(path, JSONBig.stringify(Database.initial()))); + this.transactions = new Transactions_1.default(this.path, this._data.transactions); + } + static initial() { + return { + transactions: [], + }; + } + save() { + this._data.transactions = this.transactions.all(); + fs.writeFileSync(this.path, JSONBig.stringify(this._data)); + } +} +exports.default = Database; diff --git a/cli/src/classes/Database.ts b/cli/src/classes/Database.ts new file mode 100644 index 0000000..67d4fe0 --- /dev/null +++ b/cli/src/classes/Database.ts @@ -0,0 +1,34 @@ +import * as JSONBig from 'json-bigint'; +import * as fs from "fs"; + +import {SentTx} from "../utils/Globals"; + +import Transactions from "./Transactions"; +import DataDirectory from "./DataDirectory"; + + +interface Schema { + transactions: SentTx[], +} + +export default class Database { + + public transactions: Transactions; + readonly _data: Schema; + + constructor(readonly path: string) { + this._data = JSONBig.parse(DataDirectory.createOrReadFile(path, JSONBig.stringify(Database.initial()))); + this.transactions = new Transactions(this.path, this._data.transactions); + } + + static initial() { + return { + transactions: [], + } + } + + save() { + this._data.transactions = this.transactions.all(); + fs.writeFileSync(this.path, JSONBig.stringify(this._data)); + } +} \ No newline at end of file diff --git a/cli/src/classes/Keystore.js b/cli/src/classes/Keystore.js new file mode 100644 index 0000000..a4804b5 --- /dev/null +++ b/cli/src/classes/Keystore.js @@ -0,0 +1,70 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const fs = require("fs"); +const path = require("path"); +const JSONBig = require("json-bigint"); +const Globals_1 = require("../utils/Globals"); +const lib_1 = require("../../../lib"); +class Keystore { + constructor(path, password) { + this.path = path; + this.password = password; + } + decrypt(connection) { + let accounts = []; + let promises = []; + fs.readdirSync(this.path).forEach((file) => { + if (!file.startsWith('.')) { + let keystoreFile = path.join(this.path, file); + let v3JSONKeyStore = JSONBig.parse(fs.readFileSync(keystoreFile, 'utf8')); + let decryptedAccount = lib_1.Account.decrypt(v3JSONKeyStore, this.password); + promises.push(connection.api.getAccount(decryptedAccount.address) + .then(({ balance, nonce }) => { + decryptedAccount.nonce = nonce; + decryptedAccount.balance = balance; + accounts.push(decryptedAccount); + })); + } + }); + return Promise.all(promises) + .then(() => { + return new Promise(resolve => { + resolve(accounts); + }); + }) + .catch(() => { + return new Promise(resolve => { + resolve([]); + }); + }); + } + create(outputPath, pass) { + let account = lib_1.Account.create(); + let output = this.path; + let password = this.password; + if (outputPath) { + if (fs.existsSync(outputPath)) { + output = outputPath; + } + else { + Globals_1.default.warning(`Output path provided does not exists: ${outputPath}. Using default...`); + } + } + if (pass) { + if (fs.existsSync(pass)) { + password = fs.readFileSync(pass, 'utf8'); + } + else { + Globals_1.default.warning(`Password file provided does not exists: ${pass}. Using default...`); + } + } + let encryptedAccount = account.encrypt(password); + let stringEncryptedAccount = JSONBig.stringify(encryptedAccount); + let fileName = `UTC--${JSONBig.stringify(new Date())}--${account.address}` + .replace(/"/g, '') + .replace(/:/g, '-'); + fs.writeFileSync(path.join(output, fileName), stringEncryptedAccount); + return stringEncryptedAccount; + } +} +exports.default = Keystore; diff --git a/cli/src/classes/Keystore.ts b/cli/src/classes/Keystore.ts new file mode 100644 index 0000000..d1ce800 --- /dev/null +++ b/cli/src/classes/Keystore.ts @@ -0,0 +1,82 @@ +import * as fs from "fs"; +import * as path from "path"; +import * as JSONBig from 'json-bigint'; + +import Globals from "../utils/Globals"; + +import {Account, Controller} from "../../../lib"; + + +export default class Keystore { + + constructor(readonly path: string, readonly password: string) { + } + + decrypt(connection: Controller): Promise { + let accounts = []; + let promises = []; + + fs.readdirSync(this.path).forEach((file) => { + if (!file.startsWith('.')) { + let keystoreFile = path.join(this.path, file); + let v3JSONKeyStore = JSONBig.parse(fs.readFileSync(keystoreFile, 'utf8')); + let decryptedAccount: Account = Account.decrypt(v3JSONKeyStore, this.password); + + promises.push( + connection.api.getAccount(decryptedAccount.address) + .then(({balance, nonce}) => { + decryptedAccount.nonce = nonce; + decryptedAccount.balance = balance; + accounts.push(decryptedAccount); + }) + ); + } + }); + + return Promise.all(promises) + .then(() => { + return new Promise(resolve => { + resolve(accounts); + }); + }) + .catch(() => { + return new Promise(resolve => { + resolve([]) + }) + }) + } + + create(outputPath: string, pass: string): string { + let account: Account = Account.create(); + + let output = this.path; + let password = this.password; + + if (outputPath) { + if (fs.existsSync(outputPath)) { + output = outputPath; + } else { + Globals.warning(`Output path provided does not exists: ${outputPath}. Using default...`); + } + } + + if (pass) { + if (fs.existsSync(pass)) { + password = fs.readFileSync(pass, 'utf8'); + } else { + Globals.warning(`Password file provided does not exists: ${pass}. Using default...`); + } + } + + let encryptedAccount = account.encrypt(password); + let stringEncryptedAccount = JSONBig.stringify(encryptedAccount); + let fileName = `UTC--${JSONBig.stringify(new Date())}--${account.address}` + .replace(/"/g, '') + .replace(/:/g, '-'); + + fs.writeFileSync(path.join(output, fileName), stringEncryptedAccount); + + return stringEncryptedAccount; + } + +} \ No newline at end of file diff --git a/cli/src/classes/Log.js b/cli/src/classes/Log.js new file mode 100644 index 0000000..efeaf2a --- /dev/null +++ b/cli/src/classes/Log.js @@ -0,0 +1,37 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const fs = require("fs"); +const DataDirectory_1 = require("./DataDirectory"); +class Log { + constructor(path) { + this.path = path; + DataDirectory_1.default.createOrReadFile(this.path, ''); + this._log = ``; + this._command = ``; + } + withCommand(command) { + let today = new Date(); + let date = today.getFullYear() + '-' + (today.getMonth() + 1) + '-' + today.getDate(); + let time = today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds(); + this._log += `[${date} ${time}] `; + this._log += command; + this._command = command; + return this; + } + append(keyword, description) { + this._append(`${keyword}: ${description}`); + return this; + } + show() { + console.log(this._log); + } + write() { + let previous = fs.readFileSync(this.path, 'utf8') + '\n'; + fs.writeFileSync(this.path, previous + this._log); + return this; + } + _append(text) { + this._log += `\n${text}`; + } +} +exports.default = Log; diff --git a/cli/src/classes/Log.ts b/cli/src/classes/Log.ts new file mode 100644 index 0000000..ba9f7d9 --- /dev/null +++ b/cli/src/classes/Log.ts @@ -0,0 +1,46 @@ +import * as fs from "fs"; +import DataDirectory from "./DataDirectory"; + +export default class Log { + + private _command: string; + private _log: string; + + constructor(readonly path: string) { + DataDirectory.createOrReadFile(this.path, ''); + + this._log = ``; + this._command = ``; + } + + withCommand(command: string): this { + let today = new Date(); + let date = today.getFullYear() + '-' + (today.getMonth() + 1) + '-' + today.getDate(); + let time = today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds(); + + this._log += `[${date} ${time}] `; + this._log += command; + this._command = command; + return this + } + + append(keyword: string, description: string): this { + this._append(`${keyword}: ${description}`); + return this; + } + + show(): void { + console.log(this._log); + } + + write(): this { + let previous = fs.readFileSync(this.path, 'utf8') + '\n'; + fs.writeFileSync(this.path, previous + this._log); + return this; + } + + private _append(text: string): void { + this._log += `\n${text}` + } + +} \ No newline at end of file diff --git a/cli/src/classes/Session.js b/cli/src/classes/Session.js new file mode 100644 index 0000000..4b88c4e --- /dev/null +++ b/cli/src/classes/Session.js @@ -0,0 +1,51 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const fs = require("fs"); +const lib_1 = require("../../../lib"); +const DataDirectory_1 = require("./DataDirectory"); +const Database_1 = require("./Database"); +const path = require("path"); +const Log_1 = require("./Log"); +class Session { + constructor(dataDirPath) { + this.interactive = false; + this.connection = null; + this.logs = []; + this.logpath = path.join(dataDirPath, 'logs'); + this.directory = new DataDirectory_1.default(dataDirPath); + this.database = new Database_1.default(path.join(dataDirPath, 'db.json')); + this.config = this.directory.createAndGetConfig(); + this.passwordPath = this.config.getOrCreatePasswordFile(); + this.keystore = this.config.getOrCreateKeystore(this.password); + } + get password() { + return fs.readFileSync(this.passwordPath, 'utf8'); + } + connect(forcedHost, forcedPort) { + let host = forcedHost || this.config.data.connection.host || '127.0.0.1'; + let port = forcedPort || this.config.data.connection.port || 8080; + let node = new lib_1.Controller(host, port); + return node.api.testConnection() + .then((success) => { + if (success) { + if (this.connection) { + return this.connection; + } + if (!forcedHost && !forcedPort) { + this.connection = node; + } + return node; + } + else { + return null; + } + }); + } + ; + log() { + let log = new Log_1.default(this.logpath); + this.logs.push(log); + return log; + } +} +exports.default = Session; diff --git a/cli/src/classes/Session.ts b/cli/src/classes/Session.ts new file mode 100644 index 0000000..45a152d --- /dev/null +++ b/cli/src/classes/Session.ts @@ -0,0 +1,74 @@ +import * as fs from "fs"; + +import {Controller} from "../../../lib"; + +import Config from "./Config"; +import DataDirectory from "./DataDirectory"; +import Keystore from "./Keystore"; +import Database from "./Database"; +import * as path from "path"; +import Log from "./Log"; +import Globals from "../utils/Globals"; + + +export default class Session { + + public interactive: boolean; + public passwordPath: string; + public logpath: string; + + public directory: DataDirectory; + public connection: Controller; + public keystore: Keystore; + public config: Config; + public database: Database; + public logs: Log[]; + + + constructor(dataDirPath: string) { + this.interactive = false; + this.connection = null; + this.logs = []; + this.logpath = path.join(dataDirPath, 'logs'); + + this.directory = new DataDirectory(dataDirPath); + this.database = new Database(path.join(dataDirPath, 'db.json')); + + this.config = this.directory.createAndGetConfig(); + this.passwordPath = this.config.getOrCreatePasswordFile(); + this.keystore = this.config.getOrCreateKeystore(this.password); + } + + get password(): string { + return fs.readFileSync(this.passwordPath, 'utf8'); + } + + connect(forcedHost: string, forcedPort: number): Promise { + let host: string = forcedHost || this.config.data.connection.host || '127.0.0.1'; + let port: number = forcedPort || this.config.data.connection.port || 8080; + let node = new Controller(host, port); + + return node.api.testConnection() + .then((success: boolean) => { + if (success) { + if (this.connection) { + return this.connection + } + + if (!forcedHost && !forcedPort) { + this.connection = node; + } + return node; + } else { + return null; + } + }) + }; + + log(): Log { + let log = new Log(this.logpath); + this.logs.push(log); + return log; + } + +} \ No newline at end of file diff --git a/cli/src/classes/Transactions.js b/cli/src/classes/Transactions.js new file mode 100644 index 0000000..a3eeb63 --- /dev/null +++ b/cli/src/classes/Transactions.js @@ -0,0 +1,30 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +class Transactions { + constructor(dbPath, _transactions) { + this.dbPath = dbPath; + this._transactions = _transactions; + this.sort(); + } + all() { + return this._transactions; + } + add(tx) { + delete tx.chainId; + delete tx.data; + tx.value = parseInt(tx.value, 16); + tx.gas = parseInt(tx.gas, 16); + tx.gasPrice = parseInt(tx.gasPrice, 16); + tx.nonce = parseInt(tx.nonce, 16); + tx.date = new Date(); + this._transactions.push(tx); + this.sort(); + } + sort() { + this._transactions.sort(function (a, b) { + // @ts-ignore + return new Date(b.date) - new Date(a.date); + }); + } +} +exports.default = Transactions; diff --git a/cli/src/classes/Transactions.ts b/cli/src/classes/Transactions.ts new file mode 100644 index 0000000..9009af5 --- /dev/null +++ b/cli/src/classes/Transactions.ts @@ -0,0 +1,35 @@ +import {SentTx} from "../utils/Globals"; + + +export default class Transactions { + + constructor(private dbPath: string, private _transactions: SentTx[]) { + this.sort(); + } + + all(): SentTx[] { + return this._transactions; + } + + add(tx: any): void { + delete tx.chainId; + delete tx.data; + + tx.value = parseInt(tx.value, 16); + tx.gas = parseInt(tx.gas, 16); + tx.gasPrice = parseInt(tx.gasPrice, 16); + tx.nonce = parseInt(tx.nonce, 16); + tx.date = new Date(); + + this._transactions.push(tx); + this.sort(); + } + + sort() { + this._transactions.sort(function (a, b) { + // @ts-ignore + return new Date(b.date) - new Date(a.date); + }); + } + +} diff --git a/cli/src/commands/AccountsCreate.js b/cli/src/commands/AccountsCreate.js new file mode 100644 index 0000000..8620478 --- /dev/null +++ b/cli/src/commands/AccountsCreate.js @@ -0,0 +1,53 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const inquirer = require("inquirer"); +const Globals_1 = require("../utils/Globals"); +function commandAccountsCreate(evmlc, session) { + let description = 'Allows you to create and encrypt accounts locally. Created accounts will either be placed in the' + + ' keystore folder inside the data directory provided by the global --datadir, -d flag or if no flag is' + + ' provided, in the keystore specified in the configuration file.'; + return evmlc.command('accounts create').alias('a c') + .description(description) + .option('-o, --output ', 'provide output path') + .option('-p, --password ', 'provide password file path') + .option('-i, --interactive', 'use interactive mode') + .types({ + string: ['p', 'password', 'o', 'output'] + }) + .action((args) => { + return new Promise((resolve) => __awaiter(this, void 0, void 0, function* () { + let interactive = args.options.interactive || session.interactive; + let questions = [ + { + name: 'output', + message: 'Enter keystore output path: ', + default: session.keystore.path, + type: 'input' + }, + { + name: 'password', + message: 'Enter password file path: ', + default: session.passwordPath, + type: 'input' + } + ]; + if (interactive) { + let { output, password } = yield inquirer.prompt(questions); + args.options.output = output; + args.options.password = password; + } + Globals_1.default.success(session.keystore.create(args.options.output, args.options.password)); + resolve(); + })); + }); +} +exports.default = commandAccountsCreate; +; diff --git a/cli/src/commands/AccountsCreate.ts b/cli/src/commands/AccountsCreate.ts new file mode 100644 index 0000000..9146bed --- /dev/null +++ b/cli/src/commands/AccountsCreate.ts @@ -0,0 +1,51 @@ +import * as Vorpal from "vorpal"; +import * as inquirer from 'inquirer'; + +import Globals from "../utils/Globals"; +import Session from "../classes/Session"; + + +export default function commandAccountsCreate(evmlc: Vorpal, session: Session) { + + let description = + 'Allows you to create and encrypt accounts locally. Created accounts will either be placed in the' + + ' keystore folder inside the data directory provided by the global --datadir, -d flag or if no flag is' + + ' provided, in the keystore specified in the configuration file.'; + + return evmlc.command('accounts create').alias('a c') + .description(description) + .option('-o, --output ', 'provide output path') + .option('-p, --password ', 'provide password file path') + .option('-i, --interactive', 'use interactive mode') + .types({ + string: ['p', 'password', 'o', 'output'] + }) + .action((args: Vorpal.Args): Promise => { + return new Promise(async (resolve) => { + let interactive = args.options.interactive || session.interactive; + let questions = [ + { + name: 'output', + message: 'Enter keystore output path: ', + default: session.keystore.path, + type: 'input' + }, + { + name: 'password', + message: 'Enter password file path: ', + default: session.passwordPath, + type: 'input' + } + ]; + + if (interactive) { + let {output, password} = await inquirer.prompt(questions); + args.options.output = output; + args.options.password = password; + } + + Globals.success(session.keystore.create(args.options.output, args.options.password)); + resolve(); + }) + }); +}; \ No newline at end of file diff --git a/cli/src/commands/AccountsGet.js b/cli/src/commands/AccountsGet.js new file mode 100644 index 0000000..6ecb955 --- /dev/null +++ b/cli/src/commands/AccountsGet.js @@ -0,0 +1,65 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const JSONBig = require("json-bigint"); +const inquirer = require("inquirer"); +const ASCIITable = require("ascii-table"); +const Globals_1 = require("../utils/Globals"); +function commandAccountsGet(evmlc, session) { + let description = 'Gets account balance and nonce from a node with a valid connection.'; + return evmlc.command('accounts get [address]').alias('a g') + .description(description) + .option('-f, --formatted', 'format output') + .option('-i, --interactive', 'use interactive mode') + .option('-h, --host ', 'override config parameter host') + .option('-p, --port ', 'override config parameter port') + .types({ + string: ['_', 'h', 'host'] + }) + .action((args) => { + return new Promise((resolve) => __awaiter(this, void 0, void 0, function* () { + let connection = yield session.connect(args.options.host, args.options.port); + if (!connection) + resolve(); + let interactive = args.options.interactive || session.interactive; + let formatted = args.options.formatted || false; + let questions = [ + { + name: 'address', + type: 'input', + required: true, + message: 'Address: ' + } + ]; + if (interactive) { + let { address } = yield inquirer.prompt(questions); + args.address = address; + } + if (!args.address) { + Globals_1.default.error('Provide an address. Usage: accounts get
'); + resolve(); + } + let account = yield connection.api.getAccount(args.address); + if (account) { + if (formatted) { + let table = new ASCIITable().setHeading('Address', 'Balance', 'Nonce'); + table.addRow(account.address, account.balance, account.nonce); + Globals_1.default.success(table.toString()); + } + else { + Globals_1.default.success(JSONBig.stringify(account)); + } + } + resolve(); + })); + }); +} +exports.default = commandAccountsGet; +; diff --git a/cli/src/commands/AccountsGet.ts b/cli/src/commands/AccountsGet.ts new file mode 100644 index 0000000..274f199 --- /dev/null +++ b/cli/src/commands/AccountsGet.ts @@ -0,0 +1,67 @@ +import * as Vorpal from "vorpal"; +import * as JSONBig from 'json-bigint'; +import * as inquirer from 'inquirer'; +import * as ASCIITable from 'ascii-table'; + +import Globals, {BaseAccount} from "../utils/Globals"; +import Session from "../classes/Session"; + + +export default function commandAccountsGet(evmlc: Vorpal, session: Session) { + + let description = + 'Gets account balance and nonce from a node with a valid connection.'; + + return evmlc.command('accounts get [address]').alias('a g') + .description(description) + .option('-f, --formatted', 'format output') + .option('-i, --interactive', 'use interactive mode') + .option('-h, --host ', 'override config parameter host') + .option('-p, --port ', 'override config parameter port') + .types({ + string: ['_', 'h', 'host'] + }) + .action((args: Vorpal.Args): Promise => { + return new Promise(async (resolve) => { + let connection = await session.connect(args.options.host, args.options.port); + + if (!connection) resolve(); + + let interactive = args.options.interactive || session.interactive; + let formatted = args.options.formatted || false; + let questions = [ + { + name: 'address', + type: 'input', + required: true, + message: 'Address: ' + } + ]; + + if (interactive) { + let {address} = await inquirer.prompt(questions); + args.address = address; + } + + if (!args.address) { + Globals.error('Provide an address. Usage: accounts get
'); + resolve(); + } + + let account = await connection.api.getAccount(args.address); + + if (account) { + if (formatted) { + let table = new ASCIITable().setHeading('Address', 'Balance', 'Nonce'); + table.addRow(account.address, account.balance, account.nonce); + Globals.success(table.toString()); + } else { + Globals.success(JSONBig.stringify(account)) + } + } + + resolve(); + }); + }); + +}; \ No newline at end of file diff --git a/cli/src/commands/AccountsList.js b/cli/src/commands/AccountsList.js new file mode 100644 index 0000000..521fa2f --- /dev/null +++ b/cli/src/commands/AccountsList.js @@ -0,0 +1,59 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const JSONBig = require("json-bigint"); +const ASCIITable = require("ascii-table"); +const Globals_1 = require("../utils/Globals"); +function commandAccountsList(evmlc, session) { + let description = 'List all accounts in the local keystore directory provided by the configuration file. This command will ' + + 'also get a balance and nonce for all the accounts from the node if a valid connection is established.'; + return evmlc.command('accounts list').alias('a l') + .description(description) + .option('-f, --formatted', 'format output') + .option('-r, --remote', 'list remote accounts') + .option('-h, --host ', 'override config parameter host') + .option('-p, --port ', 'override config parameter port') + .types({ + string: ['h', 'host'] + }) + .action((args) => { + return new Promise((resolve) => __awaiter(this, void 0, void 0, function* () { + let connection = yield session.connect(args.options.host, args.options.port); + if (!connection) + resolve(); + let formatted = args.options.formatted || false; + let remote = args.options.remote || false; + let accounts = []; + let accountsTable = new ASCIITable().setHeading('#', 'Address', 'Balance', 'Nonce'); + if (!remote) { + accounts = (yield session.keystore.decrypt(connection)).map(account => account.toBaseAccount()); + } + else { + accounts = yield connection.api.getAccounts(); + } + if (!accounts || !accounts.length) { + Globals_1.default.warning('No accounts.'); + } + else { + if (formatted) { + let counter = 1; + for (let account of accounts) { + accountsTable.addRow(counter, account.address, account.balance, account.nonce); + counter++; + } + } + Globals_1.default.success((formatted) ? accountsTable.toString() : JSONBig.stringify(accounts)); + } + resolve(); + })); + }); +} +exports.default = commandAccountsList; +; diff --git a/cli/src/commands/AccountsList.ts b/cli/src/commands/AccountsList.ts new file mode 100644 index 0000000..1708e10 --- /dev/null +++ b/cli/src/commands/AccountsList.ts @@ -0,0 +1,60 @@ +import * as Vorpal from "vorpal"; +import * as JSONBig from 'json-bigint'; +import * as ASCIITable from 'ascii-table'; + +import Globals, {BaseAccount} from "../utils/Globals"; + +import Session from "../classes/Session"; + + +export default function commandAccountsList(evmlc: Vorpal, session: Session) { + + let description = + 'List all accounts in the local keystore directory provided by the configuration file. This command will ' + + 'also get a balance and nonce for all the accounts from the node if a valid connection is established.'; + + return evmlc.command('accounts list').alias('a l') + .description(description) + .option('-f, --formatted', 'format output') + .option('-r, --remote', 'list remote accounts') + .option('-h, --host ', 'override config parameter host') + .option('-p, --port ', 'override config parameter port') + .types({ + string: ['h', 'host'] + }) + .action((args: Vorpal.Args): Promise => { + return new Promise(async (resolve) => { + let connection = await session.connect(args.options.host, args.options.port); + + if (!connection) resolve(); + + let formatted: boolean = args.options.formatted || false; + let remote = args.options.remote || false; + let accounts: BaseAccount[] = []; + let accountsTable = new ASCIITable().setHeading('#', 'Address', 'Balance', 'Nonce'); + + if (!remote) { + accounts = (await session.keystore.decrypt(connection)).map(account => account.toBaseAccount()); + } else { + accounts = await connection.api.getAccounts(); + } + + if (!accounts || !accounts.length) { + Globals.warning('No accounts.'); + } else { + if (formatted) { + let counter = 1; + for (let account of accounts) { + accountsTable.addRow(counter, account.address, account.balance, account.nonce); + counter++; + } + } + + Globals.success((formatted) ? accountsTable.toString() : JSONBig.stringify(accounts)); + } + + resolve(); + }); + }); + +}; \ No newline at end of file diff --git a/cli/src/commands/ConfigSet.js b/cli/src/commands/ConfigSet.js new file mode 100644 index 0000000..263b23b --- /dev/null +++ b/cli/src/commands/ConfigSet.js @@ -0,0 +1,120 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const inquirer = require("inquirer"); +const Globals_1 = require("../utils/Globals"); +function commandConfigSet(evmlc, session) { + let description = 'Set values of the configuration inside the data directory.'; + return evmlc.command('config set').alias('c s') + .description(description) + .option('-i, --interactive', 'enter into interactive command') + .option('-h, --host ', 'default host') + .option('-p, --port ', 'default port') + .option('--from ', 'default from') + .option('--gas ', 'default gas') + .option('--gasprice ', 'gas price') + .option('--keystore ', 'keystore path') + .option('--pwd ', 'password path') + .types({ + string: ['h', 'host', 'from', 'keystore', 'pwd'] + }) + .action((args) => { + return new Promise((resolve) => __awaiter(this, void 0, void 0, function* () { + try { + let interactive = args.options.interactive || session.interactive; + let questions = []; + function populateQuestions(object) { + for (let key in object) { + if (object.hasOwnProperty(key)) { + if (typeof object[key] === 'object') { + populateQuestions(object[key]); + } + else { + questions.push({ + name: key, + default: object[key], + type: 'input', + message: `${key}: ` + }); + } + } + } + } + populateQuestions(session.config.data); + if (interactive) { + let answers = yield inquirer.prompt(questions); + args.options.host = answers.host; + args.options.port = answers.port; + args.options.from = answers.from; + args.options.gas = answers.gas; + args.options.gasprice = answers.gasPrice; + args.options.keystore = answers.keystore; + args.options.password = answers.password; + } + if (!Object.keys(args.options).length) { + Globals_1.default.error('No options provided. To enter interactive mode use: -i, --interactive.'); + } + else { + for (let prop in args.options) { + if (prop.toLowerCase() === 'host') { + if (session.config.data.connection.host !== args.options[prop]) { + Globals_1.default.success(`Updated '${(prop)}' with value ${(args.options[prop])}.`); + } + session.config.data.connection.host = args.options[prop]; + } + if (prop.toLowerCase() === 'port') { + if (session.config.data.connection.port !== args.options[prop]) { + Globals_1.default.success(`Updated '${(prop)}' with value ${(args.options[prop])}.`); + } + session.config.data.connection.port = args.options[prop]; + } + if (prop.toLowerCase() === 'from') { + if (session.config.data.defaults.from !== args.options[prop]) { + Globals_1.default.success(`Updated '${(prop)}' with value ${(args.options[prop])}.`); + } + session.config.data.defaults.from = args.options[prop]; + } + if (prop.toLowerCase() === 'gas') { + if (session.config.data.defaults.gas !== args.options[prop]) { + Globals_1.default.success(`Updated '${(prop)}' with value ${(args.options[prop])}.`); + } + session.config.data.defaults.gas = args.options[prop]; + } + if (prop.toLowerCase() === 'gasprice') { + if (session.config.data.defaults.gasPrice !== args.options[prop]) { + Globals_1.default.success(`Updated '${(prop)}' with value ${(args.options[prop])}.`); + } + session.config.data.defaults.gasPrice = args.options[prop]; + } + if (prop.toLowerCase() === 'keystore') { + if (session.config.data.storage.keystore !== args.options[prop]) { + Globals_1.default.success(`Updated '${(prop)}' with value ${(args.options[prop])}.`); + } + session.config.data.storage.keystore = args.options[prop]; + } + if (prop.toLowerCase() === 'password') { + if (session.config.data.storage.password !== args.options[prop]) { + Globals_1.default.success(`Updated '${(prop)}' with value ${(args.options[prop])}.`); + } + session.config.data.storage.password = args.options[prop]; + } + } + session.config.save(); + } + } + catch (err) { + (typeof err === 'object') ? console.log(err) : Globals_1.default.error(err); + } + resolve(); + })); + }); +} +exports.default = commandConfigSet; +; diff --git a/cli/src/commands/ConfigSet.ts b/cli/src/commands/ConfigSet.ts new file mode 100644 index 0000000..42be0db --- /dev/null +++ b/cli/src/commands/ConfigSet.ts @@ -0,0 +1,126 @@ +import * as Vorpal from "vorpal"; +import * as inquirer from 'inquirer'; + +import Globals from "../utils/Globals"; +import Session from "../classes/Session"; + + +export default function commandConfigSet(evmlc: Vorpal, session: Session) { + + let description = + 'Set values of the configuration inside the data directory.'; + + return evmlc.command('config set').alias('c s') + .description(description) + .option('-i, --interactive', 'enter into interactive command') + .option('-h, --host ', 'default host') + .option('-p, --port ', 'default port') + .option('--from ', 'default from') + .option('--gas ', 'default gas') + .option('--gasprice ', 'gas price') + .option('--keystore ', 'keystore path') + .option('--pwd ', 'password path') + .types({ + string: ['h', 'host', 'from', 'keystore', 'pwd'] + }) + .action((args: Vorpal.Args): Promise => { + return new Promise(async (resolve) => { + try { + let interactive = args.options.interactive || session.interactive; + let questions = []; + + function populateQuestions(object) { + for (let key in object) { + if (object.hasOwnProperty(key)) { + if (typeof object[key] === 'object') { + populateQuestions(object[key]); + } else { + questions.push({ + name: key, + default: object[key], + type: 'input', + message: `${key}: ` + }); + } + } + } + } + + populateQuestions(session.config.data); + + if (interactive) { + let answers = await inquirer.prompt(questions); + + args.options.host = answers.host; + args.options.port = answers.port; + args.options.from = answers.from; + args.options.gas = answers.gas; + args.options.gasprice = answers.gasPrice; + args.options.keystore = answers.keystore; + args.options.password = answers.password; + } + + if (!Object.keys(args.options).length) { + Globals.error('No options provided. To enter interactive mode use: -i, --interactive.'); + } else { + for (let prop in args.options) { + if (prop.toLowerCase() === 'host') { + if (session.config.data.connection.host !== args.options[prop]) { + Globals.success(`Updated '${(prop)}' with value ${(args.options[prop])}.`); + } + + session.config.data.connection.host = args.options[prop]; + } + if (prop.toLowerCase() === 'port') { + if (session.config.data.connection.port !== args.options[prop]) { + Globals.success(`Updated '${(prop)}' with value ${(args.options[prop])}.`); + } + + session.config.data.connection.port = args.options[prop]; + } + if (prop.toLowerCase() === 'from') { + if (session.config.data.defaults.from !== args.options[prop]) { + Globals.success(`Updated '${(prop)}' with value ${(args.options[prop])}.`); + } + + session.config.data.defaults.from = args.options[prop]; + } + if (prop.toLowerCase() === 'gas') { + if (session.config.data.defaults.gas !== args.options[prop]) { + Globals.success(`Updated '${(prop)}' with value ${(args.options[prop])}.`); + } + + session.config.data.defaults.gas = args.options[prop]; + } + if (prop.toLowerCase() === 'gasprice') { + if (session.config.data.defaults.gasPrice !== args.options[prop]) { + Globals.success(`Updated '${(prop)}' with value ${(args.options[prop])}.`); + } + + session.config.data.defaults.gasPrice = args.options[prop]; + } + if (prop.toLowerCase() === 'keystore') { + if (session.config.data.storage.keystore !== args.options[prop]) { + Globals.success(`Updated '${(prop)}' with value ${(args.options[prop])}.`); + } + + session.config.data.storage.keystore = args.options[prop]; + } + if (prop.toLowerCase() === 'password') { + if (session.config.data.storage.password !== args.options[prop]) { + Globals.success(`Updated '${(prop)}' with value ${(args.options[prop])}.`); + } + + session.config.data.storage.password = args.options[prop]; + } + } + session.config.save(); + } + } catch (err) { + (typeof err === 'object') ? console.log(err) : Globals.error(err); + } + resolve(); + }); + }); + +}; \ No newline at end of file diff --git a/cli/src/commands/ConfigView.js b/cli/src/commands/ConfigView.js new file mode 100644 index 0000000..7cc7d4c --- /dev/null +++ b/cli/src/commands/ConfigView.js @@ -0,0 +1,22 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const Globals_1 = require("../utils/Globals"); +function commandConfigUser(evmlc, session) { + let description = 'Output current configuration file as JSON.'; + return evmlc.command('config view').alias('c v') + .description(description) + .action(() => { + return new Promise(resolve => { + try { + Globals_1.default.info(`Config file location: ${session.config.path}`); + Globals_1.default.success(session.config.toTOML()); + } + catch (err) { + (typeof err === 'object') ? console.log(err) : Globals_1.default.error(err); + } + resolve(); + }); + }); +} +exports.default = commandConfigUser; +; diff --git a/cli/src/commands/ConfigView.ts b/cli/src/commands/ConfigView.ts new file mode 100644 index 0000000..4b965af --- /dev/null +++ b/cli/src/commands/ConfigView.ts @@ -0,0 +1,26 @@ +import * as Vorpal from "vorpal"; + +import Globals from "../utils/Globals"; +import Session from "../classes/Session"; + + +export default function commandConfigUser(evmlc: Vorpal, session: Session) { + + let description = + 'Output current configuration file as JSON.'; + + return evmlc.command('config view').alias('c v') + .description(description) + .action((): Promise => { + return new Promise(resolve => { + try { + Globals.info(`Config file location: ${session.config.path}`); + Globals.success(session.config.toTOML()); + } catch (err) { + (typeof err === 'object') ? console.log(err) : Globals.error(err); + } + resolve(); + }); + }); + +}; \ No newline at end of file diff --git a/cli/src/commands/Info.js b/cli/src/commands/Info.js new file mode 100644 index 0000000..f2adebc --- /dev/null +++ b/cli/src/commands/Info.js @@ -0,0 +1,49 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const JSONBig = require("json-bigint"); +const ASCIITable = require("ascii-table"); +const Globals_1 = require("../utils/Globals"); +function commandInfo(evmlc, session) { + return evmlc.command('info') + .description('Prints information about node as JSON or --formatted.') + .option('-f, --formatted', 'format output') + .option('-h, --host ', 'override config parameter host') + .option('-p, --port ', 'override config parameter port') + .types({ + string: ['h', 'host'] + }) + .action((args) => { + return new Promise((resolve) => __awaiter(this, void 0, void 0, function* () { + let connection = yield session.connect(args.options.host, args.options.port); + if (!connection) + resolve(); + let formatted = args.options.formatted || false; + let table = new ASCIITable().setHeading('Name', 'Value'); + let information = yield connection.api.getInfo(); + if (information) { + if (formatted) { + for (let key in information) { + if (information.hasOwnProperty(key)) { + table.addRow(key, information[key]); + } + } + Globals_1.default.success(table.toString()); + } + else { + Globals_1.default.success(JSONBig.stringify(information)); + } + } + resolve(); + })); + }); +} +exports.default = commandInfo; +; diff --git a/cli/src/commands/Info.ts b/cli/src/commands/Info.ts new file mode 100644 index 0000000..90fb1bd --- /dev/null +++ b/cli/src/commands/Info.ts @@ -0,0 +1,46 @@ +import * as Vorpal from "vorpal"; +import * as JSONBig from 'json-bigint'; +import * as ASCIITable from 'ascii-table'; + +import Session from "../classes/Session"; +import Globals from "../utils/Globals"; + + +export default function commandInfo(evmlc: Vorpal, session: Session) { + return evmlc.command('info') + .description('Prints information about node as JSON or --formatted.') + .option('-f, --formatted', 'format output') + .option('-h, --host ', 'override config parameter host') + .option('-p, --port ', 'override config parameter port') + .types({ + string: ['h', 'host'] + }) + .action((args: Vorpal.Args): Promise => { + return new Promise(async (resolve) => { + let connection = await session.connect(args.options.host, args.options.port); + + if (!connection) resolve(); + + let formatted = args.options.formatted || false; + let table = new ASCIITable().setHeading('Name', 'Value'); + + let information = await connection.api.getInfo(); + + if (information) { + if (formatted) { + for (let key in information) { + if (information.hasOwnProperty(key)) { + table.addRow(key, information[key]); + } + } + Globals.success(table.toString()); + } else { + Globals.success(JSONBig.stringify(information)); + } + } + + resolve(); + }); + }); +}; + diff --git a/cli/src/commands/Interactive.js b/cli/src/commands/Interactive.js new file mode 100644 index 0000000..baf6c50 --- /dev/null +++ b/cli/src/commands/Interactive.js @@ -0,0 +1,12 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +function commandInteractive(evmlc, session) { + let description = 'Enter into interactive mode with data directory provided by --datadir, -d or default.'; + return evmlc.command('interactive').alias('i') + .description(description) + .action(() => { + return new Promise(resolve => resolve()); + }); +} +exports.default = commandInteractive; +; diff --git a/cli/src/commands/Interactive.ts b/cli/src/commands/Interactive.ts new file mode 100644 index 0000000..3fcd366 --- /dev/null +++ b/cli/src/commands/Interactive.ts @@ -0,0 +1,16 @@ +import * as Vorpal from "vorpal"; + +import Session from "../classes/Session"; + + +export default function commandInteractive(evmlc: Vorpal, session: Session) { + + let description = + 'Enter into interactive mode with data directory provided by --datadir, -d or default.'; + + return evmlc.command('interactive').alias('i') + .description(description) + .action((): Promise => { + return new Promise(resolve => resolve()); + }); +}; \ No newline at end of file diff --git a/cli/src/commands/LogsClear.js b/cli/src/commands/LogsClear.js new file mode 100644 index 0000000..c1577ab --- /dev/null +++ b/cli/src/commands/LogsClear.js @@ -0,0 +1,23 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const fs = require("fs"); +const Globals_1 = require("../utils/Globals"); +function commandLogsClear(evmlc, session) { + return evmlc.command('logs clear').alias('l c') + .description('Clears log information.') + .hidden() + .action((args) => { + return new Promise((resolve) => { + try { + fs.writeFileSync(session.logpath, ''); + Globals_1.default.success('Logs cleared.'); + } + catch (err) { + (typeof err === 'object') ? console.log(err) : Globals_1.default.error(err); + } + resolve(); + }); + }); +} +exports.default = commandLogsClear; +; diff --git a/cli/src/commands/LogsClear.ts b/cli/src/commands/LogsClear.ts new file mode 100644 index 0000000..30b5e73 --- /dev/null +++ b/cli/src/commands/LogsClear.ts @@ -0,0 +1,24 @@ +import * as Vorpal from "vorpal"; +import * as fs from "fs"; + +import Globals from "../utils/Globals"; +import Session from "../classes/Session"; + + +export default function commandLogsClear(evmlc: Vorpal, session: Session) { + return evmlc.command('logs clear').alias('l c') + .description('Clears log information.') + .hidden() + .action((args: Vorpal.Args): Promise => { + return new Promise((resolve) => { + try { + fs.writeFileSync(session.logpath, ''); + Globals.success('Logs cleared.'); + } catch (err) { + (typeof err === 'object') ? console.log(err) : Globals.error(err); + } + resolve(); + }); + }); +}; + diff --git a/cli/src/commands/LogsView.js b/cli/src/commands/LogsView.js new file mode 100644 index 0000000..e9a6b1f --- /dev/null +++ b/cli/src/commands/LogsView.js @@ -0,0 +1,37 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const fs = require("fs"); +const Globals_1 = require("../utils/Globals"); +function commandLogsShow(evmlc, session) { + return evmlc.command('logs view').alias('l v') + .description('Prints log information to screen in plain text.') + .option('-s, --session', 'output session logs') + .hidden() + .action((args) => { + return new Promise((resolve) => { + try { + let interactive = session.interactive || false; + let current = args.options.session || false; + if (current) { + if (interactive) { + for (let log of session.logs) { + log.show(); + } + } + else { + Globals_1.default.warning('Cannot print session log when not in interactive mode.'); + } + } + else { + Globals_1.default.info(fs.readFileSync(session.logpath, 'utf8')); + } + } + catch (err) { + (typeof err === 'object') ? console.log(err) : Globals_1.default.error(err); + } + resolve(); + }); + }); +} +exports.default = commandLogsShow; +; diff --git a/cli/src/commands/LogsView.ts b/cli/src/commands/LogsView.ts new file mode 100644 index 0000000..c9e6c9e --- /dev/null +++ b/cli/src/commands/LogsView.ts @@ -0,0 +1,37 @@ +import * as Vorpal from "vorpal"; +import * as fs from "fs"; + +import Globals from "../utils/Globals"; +import Session from "../classes/Session"; + + +export default function commandLogsShow(evmlc: Vorpal, session: Session) { + return evmlc.command('logs view').alias('l v') + .description('Prints log information to screen in plain text.') + .option('-s, --session', 'output session logs') + .hidden() + .action((args: Vorpal.Args): Promise => { + return new Promise((resolve) => { + try { + let interactive = session.interactive || false; + let current = args.options.session || false; + + if (current) { + if (interactive) { + for (let log of session.logs) { + log.show(); + } + } else { + Globals.warning('Cannot print session log when not in interactive mode.'); + } + } else { + Globals.info(fs.readFileSync(session.logpath, 'utf8')); + } + } catch (err) { + (typeof err === 'object') ? console.log(err) : Globals.error(err); + } + resolve(); + }); + }); +}; + diff --git a/cli/src/commands/Test.js b/cli/src/commands/Test.js new file mode 100644 index 0000000..abb63c7 --- /dev/null +++ b/cli/src/commands/Test.js @@ -0,0 +1,23 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +function commandTest(evmlc, session) { + return evmlc.command('test').alias('test') + .hidden() + .action((args) => { + return new Promise((resolve) => __awaiter(this, void 0, void 0, function* () { + console.log(session.logs); + resolve(); + })); + }) + .description('Testing purposes.'); +} +exports.default = commandTest; +; diff --git a/cli/src/commands/Test.ts b/cli/src/commands/Test.ts new file mode 100644 index 0000000..a703b9c --- /dev/null +++ b/cli/src/commands/Test.ts @@ -0,0 +1,17 @@ +import * as Vorpal from "vorpal"; + +import Session from "../classes/Session"; + + +export default function commandTest(evmlc: Vorpal, session: Session) { + return evmlc.command('test').alias('test') + .hidden() + .action((args: Vorpal.Args): Promise => { + return new Promise(async resolve => { + console.log(session.logs); + resolve(); + }); + }) + .description('Testing purposes.'); +}; + diff --git a/cli/src/commands/TransactionsGet.js b/cli/src/commands/TransactionsGet.js new file mode 100644 index 0000000..d86719d --- /dev/null +++ b/cli/src/commands/TransactionsGet.js @@ -0,0 +1,71 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const ASCIITable = require("ascii-table"); +const inquirer = require("inquirer"); +const JSONBig = require("json-bigint"); +const Globals_1 = require("../utils/Globals"); +function commandTransactionsGet(evmlc, session) { + let description = 'Gets a transaction using its hash.'; + return evmlc.command('transactions get [hash]').alias('t g') + .description(description) + .option('-f, --formatted', 'format output') + .option('-i, --interactive', 'use interactive mode') + .option('-h, --host ', 'override config parameter host') + .option('-p, --port ', 'override config parameter port') + .types({ + string: ['_', 'h', 'host'] + }) + .action((args) => { + return new Promise((resolve) => __awaiter(this, void 0, void 0, function* () { + let connection = yield session.connect(args.options.host, args.options.port); + if (!connection) + resolve(); + let table = new ASCIITable().setHeading('Key', 'Value'); + let interactive = args.options.interactive || session.interactive; + let formatted = args.options.formatted || false; + let questions = [ + { + name: 'hash', + type: 'input', + required: true, + message: 'Transaction Hash: ' + } + ]; + if (interactive) { + let { hash } = yield inquirer.prompt(questions); + args.hash = hash; + } + if (!args.hash) { + Globals_1.default.error('Provide a transaction hash. Usage: transactions get '); + } + else { + let receipt = yield connection.api.getReceipt(args.hash); + if (!receipt) + resolve(); + delete receipt.logsBloom; + delete receipt.logs; + delete receipt.contractAddress; + delete receipt.root; + if (formatted) { + for (let key in receipt) { + if (receipt.hasOwnProperty(key)) { + table.addRow(key, receipt[key]); + } + } + } + Globals_1.default.success((formatted) ? table.toString() : JSONBig.stringify(receipt)); + } + resolve(); + })); + }); +} +exports.default = commandTransactionsGet; +; diff --git a/cli/src/commands/TransactionsGet.ts b/cli/src/commands/TransactionsGet.ts new file mode 100644 index 0000000..d195b02 --- /dev/null +++ b/cli/src/commands/TransactionsGet.ts @@ -0,0 +1,76 @@ +import * as Vorpal from "vorpal"; +import * as ASCIITable from 'ascii-table'; +import * as inquirer from 'inquirer'; +import * as JSONBig from 'json-bigint'; + +import Globals, {TXReceipt} from "../utils/Globals"; +import Session from "../classes/Session"; + + +export default function commandTransactionsGet(evmlc: Vorpal, session: Session) { + + let description = + 'Gets a transaction using its hash.'; + + return evmlc.command('transactions get [hash]').alias('t g') + .description(description) + .option('-f, --formatted', 'format output') + .option('-i, --interactive', 'use interactive mode') + .option('-h, --host ', 'override config parameter host') + .option('-p, --port ', 'override config parameter port') + .types({ + string: ['_', 'h', 'host'] + }) + .action((args: Vorpal.Args): Promise => { + return new Promise(async (resolve) => { + let connection = await session.connect(args.options.host, args.options.port); + + if (!connection) resolve(); + + let table = new ASCIITable().setHeading('Key', 'Value'); + let interactive = args.options.interactive || session.interactive; + let formatted = args.options.formatted || false; + let questions = [ + { + name: 'hash', + type: 'input', + required: true, + message: 'Transaction Hash: ' + } + ]; + + if (interactive) { + let {hash} = await inquirer.prompt(questions); + + args.hash = hash; + } + + if (!args.hash) { + Globals.error('Provide a transaction hash. Usage: transactions get '); + } else { + let receipt: TXReceipt = await connection.api.getReceipt(args.hash); + + if (!receipt) resolve(); + + delete receipt.logsBloom; + delete receipt.logs; + delete receipt.contractAddress; + delete receipt.root; + + if (formatted) { + for (let key in receipt) { + if (receipt.hasOwnProperty(key)) { + table.addRow(key, receipt[key]); + } + } + + } + + Globals.success((formatted) ? table.toString() : JSONBig.stringify(receipt)); + } + + resolve(); + }); + }); + +}; \ No newline at end of file diff --git a/cli/src/commands/TransactionsList.js b/cli/src/commands/TransactionsList.js new file mode 100644 index 0000000..f450520 --- /dev/null +++ b/cli/src/commands/TransactionsList.js @@ -0,0 +1,66 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const JSONBig = require("json-bigint"); +const ASCIITable = require("ascii-table"); +const Globals_1 = require("../utils/Globals"); +function commandTransactionsList(evmlc, session) { + let description = 'Lists all submitted transactions with the status.'; + return evmlc.command('transactions list').alias('t l') + .description(description) + .option('-f, --formatted', 'format output') + .option('-v, --verbose', 'verbose output') + .option('-h, --host ', 'override config parameter host') + .option('-p, --port ', 'override config parameter port') + .types({ + string: ['h', 'host'] + }) + .action((args) => { + return new Promise((resolve) => __awaiter(this, void 0, void 0, function* () { + let connection = yield session.connect(args.options.host, args.options.port); + if (!connection) + resolve(); + let formatted = args.options.formatted || false; + let verbose = args.options.verbose || false; + let table = new ASCIITable(); + let transactions = session.database.transactions.all(); + if (!transactions.length) { + Globals_1.default.warning('No transactions submitted.'); + resolve(); + } + if (!formatted) { + Globals_1.default.success(JSONBig.stringify(session.database.transactions.all())); + } + else { + if (verbose) { + table.setHeading('Date Time', 'Hash', 'From', 'To', 'Value', 'Gas', 'Gas Price', 'Status'); + for (let tx of transactions) { + let date = new Date(tx.date); + let d = date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate(); + let t = date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds(); + let receipt = yield connection.api.getReceipt(tx.txHash); + table.addRow(`${d} ${t}`, tx.txHash, tx.from, tx.to, tx.value, tx.gas, tx.gasPrice, (receipt) ? ((!receipt.failed) ? 'Success' : 'Failed') : 'Failed'); + } + } + else { + table.setHeading('From', 'To', 'Value', 'Status'); + for (let tx of transactions) { + let receipt = yield connection.api.getReceipt(tx.txHash); + table.addRow(tx.from, tx.to, tx.value, (receipt) ? ((!receipt.failed) ? 'Success' : 'Failed') : 'Failed'); + } + } + Globals_1.default.success(table.toString()); + } + resolve(); + })); + }); +} +exports.default = commandTransactionsList; +; diff --git a/cli/src/commands/TransactionsList.ts b/cli/src/commands/TransactionsList.ts new file mode 100644 index 0000000..74e8262 --- /dev/null +++ b/cli/src/commands/TransactionsList.ts @@ -0,0 +1,72 @@ +import * as Vorpal from "vorpal"; +import * as JSONBig from 'json-bigint'; +import * as ASCIITable from 'ascii-table'; + +import Globals, {TXReceipt} from "../utils/Globals"; +import Session from "../classes/Session"; + + +export default function commandTransactionsList(evmlc: Vorpal, session: Session) { + + let description = + 'Lists all submitted transactions with the status.'; + + return evmlc.command('transactions list').alias('t l') + .description(description) + .option('-f, --formatted', 'format output') + .option('-v, --verbose', 'verbose output') + .option('-h, --host ', 'override config parameter host') + .option('-p, --port ', 'override config parameter port') + .types({ + string: ['h', 'host'] + }) + .action((args: Vorpal.Args): Promise => { + return new Promise(async (resolve) => { + let connection = await session.connect(args.options.host, args.options.port); + + if (!connection) resolve(); + + let formatted = args.options.formatted || false; + let verbose = args.options.verbose || false; + let table = new ASCIITable(); + let transactions = session.database.transactions.all(); + + if (!transactions.length) { + Globals.warning('No transactions submitted.'); + resolve(); + } + + if (!formatted) { + Globals.success(JSONBig.stringify(session.database.transactions.all())); + } else { + if (verbose) { + table.setHeading('Date Time', 'Hash', 'From', 'To', 'Value', 'Gas', 'Gas Price', 'Status'); + + for (let tx of transactions) { + let date = new Date(tx.date); + let d = date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate(); + let t = date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds(); + let receipt: TXReceipt = await connection.api.getReceipt(tx.txHash); + + table.addRow(`${d} ${t}`, tx.txHash, tx.from, tx.to, tx.value, tx.gas, tx.gasPrice, + (receipt) ? ((!receipt.failed) ? 'Success' : 'Failed') : 'Failed'); + } + } else { + table.setHeading('From', 'To', 'Value', 'Status'); + + for (let tx of transactions) { + let receipt: TXReceipt = await connection.api.getReceipt(tx.txHash); + + table.addRow(tx.from, tx.to, tx.value, + (receipt) ? ((!receipt.failed) ? 'Success' : 'Failed') : 'Failed'); + } + } + + Globals.success(table.toString()); + } + + resolve(); + }); + }); + +}; \ No newline at end of file diff --git a/cli/src/commands/Transfer.js b/cli/src/commands/Transfer.js new file mode 100644 index 0000000..d30ce21 --- /dev/null +++ b/cli/src/commands/Transfer.js @@ -0,0 +1,110 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const inquirer = require("inquirer"); +const JSONBig = require("json-bigint"); +const Globals_1 = require("../utils/Globals"); +function commandTransfer(evmlc, session) { + let description = 'Initiate a transfer of token(s) to an address. Default values for gas and gas prices are set in the' + + ' configuration file.'; + return evmlc.command('transfer').alias('t') + .description(description) + .option('-i, --interactive', 'value to send') + .option('-v, --value ', 'value to send') + .option('-g, --gas ', 'gas to send at') + .option('-gp, --gasprice ', 'gas price to send at') + .option('-t, --to
', 'address to send to') + .option('-f, --from
', 'address to send from') + .option('-h, --host ', 'override config parameter host') + .option('-p, --port ', 'override config parameter port') + .types({ + string: ['t', 'to', 'f', 'from', 'h', 'host'], + }) + .action((args) => { + return new Promise((resolve) => __awaiter(this, void 0, void 0, function* () { + let connection = yield session.connect(args.options.host, args.options.port); + if (!connection) + resolve(); + let interactive = args.options.interactive || session.interactive; + let accounts = yield session.keystore.decrypt(connection); + let questions = [ + { + name: 'from', + type: 'list', + message: 'From: ', + choices: accounts.map((account) => account.address) + }, + { + name: 'to', + type: 'input', + message: 'To' + }, + { + name: 'value', + type: 'input', + default: '100', + message: 'Value: ' + }, + { + name: 'gas', + type: 'input', + default: session.config.data.defaults.gas || 100000, + message: 'Gas: ' + }, + { + name: 'gasPrice', + type: 'input', + default: session.config.data.defaults.gasPrice || 0, + message: 'Gas Price: ' + } + ]; + let tx = {}; + if (interactive) { + tx = yield inquirer.prompt(questions); + } + else { + tx.from = args.options.from || undefined; + tx.to = args.options.to || undefined; + tx.value = args.options.value || undefined; + tx.gas = args.options.gas || session.config.data.defaults.gas || 100000; + tx.gasPrice = args.options.gasprice || session.config.data.defaults.gasPrice || 0; + } + if (!tx.from && !tx.to && !tx.value) { + Globals_1.default.error('Provide from, to and a value.'); + resolve(); + } + let account = accounts.find((acc) => acc.address === tx.from); + if (!account) { + Globals_1.default.error('Cannot find associated local account.'); + } + else { + tx.chainId = 1; + tx.nonce = account.nonce; + let signed = yield account.signTransaction(tx); + connection.api.sendRawTx(signed.rawTransaction) + .then((resp) => { + let response = JSONBig.parse(resp); + tx.txHash = response.txHash; + session.database.transactions.add(tx); + session.database.save(); + Globals_1.default.info(`(From) ${tx.from} -> (To) ${tx.to} (${tx.value})`); + Globals_1.default.success(`Transaction submitted.`); + resolve(); + }) + .catch(() => { + Globals_1.default.error('Ran out of gas. Current Gas: ' + parseInt(tx.gas, 16)); + resolve(); + }); + } + })); + }); +} +exports.default = commandTransfer; +; diff --git a/cli/src/commands/Transfer.ts b/cli/src/commands/Transfer.ts new file mode 100644 index 0000000..082f855 --- /dev/null +++ b/cli/src/commands/Transfer.ts @@ -0,0 +1,114 @@ +import * as Vorpal from "vorpal"; +import * as inquirer from 'inquirer'; +import * as JSONBig from 'json-bigint'; + +import Globals from "../utils/Globals"; +import Session from "../classes/Session"; + + +export default function commandTransfer(evmlc: Vorpal, session: Session) { + + let description = + 'Initiate a transfer of token(s) to an address. Default values for gas and gas prices are set in the' + + ' configuration file.'; + + return evmlc.command('transfer').alias('t') + .description(description) + .option('-i, --interactive', 'value to send') + .option('-v, --value ', 'value to send') + .option('-g, --gas ', 'gas to send at') + .option('-gp, --gasprice ', 'gas price to send at') + .option('-t, --to
', 'address to send to') + .option('-f, --from
', 'address to send from') + .option('-h, --host ', 'override config parameter host') + .option('-p, --port ', 'override config parameter port') + .types({ + string: ['t', 'to', 'f', 'from', 'h', 'host'], + }) + .action((args: Vorpal.Args): Promise => { + return new Promise(async (resolve) => { + let connection = await session.connect(args.options.host, args.options.port); + + if (!connection) resolve(); + + let interactive = args.options.interactive || session.interactive; + let accounts = await session.keystore.decrypt(connection); + let questions = [ + { + name: 'from', + type: 'list', + message: 'From: ', + choices: accounts.map((account) => account.address) + }, + { + name: 'to', + type: 'input', + message: 'To' + }, + { + name: 'value', + type: 'input', + default: '100', + message: 'Value: ' + }, + { + name: 'gas', + type: 'input', + default: session.config.data.defaults.gas || 100000, + message: 'Gas: ' + }, + { + name: 'gasPrice', + type: 'input', + default: session.config.data.defaults.gasPrice || 0, + message: 'Gas Price: ' + } + ]; + let tx: any = {}; + + if (interactive) { + tx = await inquirer.prompt(questions) + } else { + tx.from = args.options.from || undefined; + tx.to = args.options.to || undefined; + tx.value = args.options.value || undefined; + tx.gas = args.options.gas || session.config.data.defaults.gas || 100000; + tx.gasPrice = args.options.gasprice || session.config.data.defaults.gasPrice || 0; + } + + if (!tx.from && !tx.to && !tx.value) { + Globals.error('Provide from, to and a value.'); + resolve(); + } + + let account = accounts.find((acc) => acc.address === tx.from); + + if (!account) { + Globals.error('Cannot find associated local account.'); + } else { + tx.chainId = 1; + tx.nonce = account.nonce; + + let signed = await account.signTransaction(tx); + + connection.api.sendRawTx(signed.rawTransaction) + .then((resp) => { + let response: any = JSONBig.parse(resp); + tx.txHash = response.txHash; + + session.database.transactions.add(tx); + session.database.save(); + + Globals.info(`(From) ${tx.from} -> (To) ${tx.to} (${tx.value})`); + Globals.success(`Transaction submitted.`); + resolve(); + }) + .catch(() => { + Globals.error('Ran out of gas. Current Gas: ' + parseInt(tx.gas, 16)); + resolve(); + }) + } + }); + }) + +}; \ No newline at end of file diff --git a/cli/src/evmlc.js b/cli/src/evmlc.js new file mode 100755 index 0000000..359990d --- /dev/null +++ b/cli/src/evmlc.js @@ -0,0 +1,85 @@ +#!/usr/bin/env node +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const Vorpal = require("vorpal"); +const fs = require("fs"); +const mkdir = require("mkdirp"); +const Globals_1 = require("./utils/Globals"); +const Session_1 = require("./classes/Session"); +const TransactionsList_1 = require("./commands/TransactionsList"); +const TransactionsGet_1 = require("./commands/TransactionsGet"); +const AccountsCreate_1 = require("./commands/AccountsCreate"); +const AccountsList_1 = require("./commands/AccountsList"); +const AccountsGet_1 = require("./commands/AccountsGet"); +const Interactive_1 = require("./commands/Interactive"); +const ConfigView_1 = require("./commands/ConfigView"); +const ConfigSet_1 = require("./commands/ConfigSet"); +const Transfer_1 = require("./commands/Transfer"); +const Test_1 = require("./commands/Test"); +const Info_1 = require("./commands/Info"); +const LogsView_1 = require("./commands/LogsView"); +const LogsClear_1 = require("./commands/LogsClear"); +const init = () => { + return new Promise(resolve => { + if (!fs.existsSync(Globals_1.default.evmlcDir)) { + mkdir.mkdirp(Globals_1.default.evmlcDir); + } + resolve(); + }); +}; +/** + * EVM-Lite Command Line Interface + */ +init() + .then(() => { + let dataDirPath = Globals_1.default.evmlcDir; + if ((process.argv[2] === '--datadir' || process.argv[2] === '-d')) { + dataDirPath = process.argv[3]; + if (!fs.existsSync(process.argv[3])) { + Globals_1.default.warning('Data directory file path provided does not exist and hence will created...'); + } + process.argv.splice(2, 2); + } + let session = new Session_1.default(dataDirPath); + if (!process.argv[2]) { + console.log(`\n A Command Line Interface to interact with EVM-Lite.`); + console.log(`\n Current Data Directory: ${session.directory.path}`); + process.argv[2] = 'help'; + } + return session; +}) + .then((session) => { + const evmlc = new Vorpal().version("0.1.0"); + [ + ConfigView_1.default, + ConfigSet_1.default, + AccountsCreate_1.default, + AccountsList_1.default, + AccountsGet_1.default, + Interactive_1.default, + Transfer_1.default, + Info_1.default, + Test_1.default, + TransactionsList_1.default, + TransactionsGet_1.default, + LogsView_1.default, + LogsClear_1.default, + ].forEach(command => { + command(evmlc, session); + }); + return { + instance: evmlc, + session: session + }; +}) + .then((cli) => { + if (process.argv[2] === 'interactive' || process.argv[2] === 'i') { + Globals_1.default.info(`Entered interactive mode with data directory: ${cli.session.directory.path}`); + cli.session.interactive = true; + cli.instance.delimiter('evmlc$').show(); + } + else { + cli.instance.parse(process.argv); + } +}) + .catch(err => Globals_1.default.error(err)); diff --git a/cli/src/evmlc.ts b/cli/src/evmlc.ts new file mode 100644 index 0000000..7c0a3a9 --- /dev/null +++ b/cli/src/evmlc.ts @@ -0,0 +1,97 @@ +#!/usr/bin/env node + +import * as Vorpal from "vorpal"; +import * as fs from "fs"; +import * as mkdir from 'mkdirp'; + +import Globals from "./utils/Globals"; +import Session from "./classes/Session"; + +import TransactionsList from "./commands/TransactionsList"; +import TransactionsGet from "./commands/TransactionsGet"; +import AccountCreate from './commands/AccountsCreate'; +import AccountsList from './commands/AccountsList'; +import AccountsGet from './commands/AccountsGet'; +import Interactive from "./commands/Interactive"; +import ConfigView from "./commands/ConfigView"; +import ConfigSet from "./commands/ConfigSet"; +import Transfer from "./commands/Transfer"; +import Test from "./commands/Test"; +import Info from "./commands/Info"; +import LogsView from "./commands/LogsView"; +import LogsClear from "./commands/LogsClear"; + + +const init = (): Promise => { + return new Promise(resolve => { + if (!fs.existsSync(Globals.evmlcDir)) { + mkdir.mkdirp(Globals.evmlcDir); + } + resolve(); + }); +}; + +/** + * EVM-Lite Command Line Interface + */ +init() + .then(() => { + let dataDirPath: string = Globals.evmlcDir; + + if ((process.argv[2] === '--datadir' || process.argv[2] === '-d')) { + dataDirPath = process.argv[3]; + + if (!fs.existsSync(process.argv[3])) { + Globals.warning('Data directory file path provided does not exist and hence will created...'); + } + + process.argv.splice(2, 2); + } + + let session = new Session(dataDirPath); + + if (!process.argv[2]) { + console.log(`\n A Command Line Interface to interact with EVM-Lite.`); + console.log(`\n Current Data Directory: ${session.directory.path}`); + + process.argv[2] = 'help'; + } + + return session; + }) + .then((session: Session) => { + const evmlc = new Vorpal().version("0.1.0"); + + [ + ConfigView, + ConfigSet, + AccountCreate, + AccountsList, + AccountsGet, + Interactive, + Transfer, + Info, + Test, + TransactionsList, + TransactionsGet, + LogsView, + LogsClear, + ].forEach(command => { + command(evmlc, session); + }); + + return { + instance: evmlc, + session: session + } + }) + .then((cli: { instance: Vorpal, session: Session }) => { + if (process.argv[2] === 'interactive' || process.argv[2] === 'i') { + Globals.info(`Entered interactive mode with data directory: ${cli.session.directory.path}`); + cli.session.interactive = true; + cli.instance.delimiter('evmlc$').show(); + } else { + cli.instance.parse(process.argv); + } + }) + .catch(err => Globals.error(err)); \ No newline at end of file diff --git a/cli/src/utils/Globals.js b/cli/src/utils/Globals.js new file mode 100644 index 0000000..e652eeb --- /dev/null +++ b/cli/src/utils/Globals.js @@ -0,0 +1,41 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const path = require("path"); +const Chalk = require("chalk"); +class Globals { + constructor() { + } + static success(message) { + console.log(Chalk.default.green(message)); + } + static warning(message) { + console.log(Chalk.default.yellow(message)); + } + static error(message) { + console.log(Chalk.default.red(message)); + } + static info(message) { + console.log(Chalk.default.blue(message)); + } + static isEquivalentObjects(objectA, objectB) { + let aProps = Object.getOwnPropertyNames(objectA); + let bProps = Object.getOwnPropertyNames(objectB); + if (aProps.length !== bProps.length) { + return false; + } + for (let i = 0; i < aProps.length; i++) { + let propName = aProps[i]; + if (typeof objectA[propName] === 'object' && typeof objectB[propName] === 'object') { + if (!Globals.isEquivalentObjects(objectA[propName], objectB[propName])) { + return false; + } + } + else if (objectA[propName] !== objectB[propName]) { + return false; + } + } + return true; + } +} +Globals.evmlcDir = path.join(require('os').homedir(), '.evmlc'); +exports.default = Globals; diff --git a/cli/src/utils/Globals.ts b/cli/src/utils/Globals.ts new file mode 100644 index 0000000..43f63c8 --- /dev/null +++ b/cli/src/utils/Globals.ts @@ -0,0 +1,83 @@ +import * as path from "path"; +import * as Chalk from "chalk"; + + +export interface BaseAccount { + address: string, + nonce: number, + balance: any +} + +export interface SentTx { + from: string, + to: string, + value: number, + gas: number, + nonce: number, + gasPrice: number, + date: any, + txHash: string +} + +export interface TXReceipt { + root: string, + transactionHash: string, + from: string, + to?: string, + gasUsed: number, + cumulativeGasUsed: number, + contractAddress: string, + logs: [], + logsBloom: string, + failed: boolean +} + + +export default class Globals { + + static evmlcDir: string = path.join(require('os').homedir(), '.evmlc'); + + constructor() { + } + + static success(message: any): void { + console.log(Chalk.default.green(message)); + } + + static warning(message: any): void { + console.log(Chalk.default.yellow(message)); + } + + static error(message: any): void { + console.log(Chalk.default.red(message)); + } + + static info(message: any): void { + console.log(Chalk.default.blue(message)); + } + + static isEquivalentObjects(objectA: any, objectB: any) { + + let aProps = Object.getOwnPropertyNames(objectA); + let bProps = Object.getOwnPropertyNames(objectB); + + if (aProps.length !== bProps.length) { + return false; + } + + for (let i = 0; i < aProps.length; i++) { + let propName = aProps[i]; + + if (typeof objectA[propName] === 'object' && typeof objectB[propName] === 'object') { + if (!Globals.isEquivalentObjects(objectA[propName], objectB[propName])) { + return false; + } + } else if (objectA[propName] !== objectB[propName]) { + return false; + } + } + + return true; + } + +} diff --git a/cli/tsconfig.json b/cli/tsconfig.json new file mode 100644 index 0000000..6a893ce --- /dev/null +++ b/cli/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs" + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules", + "lib" + ] +} \ No newline at end of file diff --git a/lib/.gitignore b/lib/.gitignore new file mode 100644 index 0000000..c96ea14 --- /dev/null +++ b/lib/.gitignore @@ -0,0 +1,4 @@ +node_modules +package-lock.json +docs +.idea diff --git a/lib/.npmignore b/lib/.npmignore new file mode 100644 index 0000000..4496812 --- /dev/null +++ b/lib/.npmignore @@ -0,0 +1,7 @@ +node_modules +tests +CHANGELOG.md +src +docs +tsconfig.json +.npmignore diff --git a/lib/@types/index.d.ts b/lib/@types/index.d.ts new file mode 100644 index 0000000..b56864a --- /dev/null +++ b/lib/@types/index.d.ts @@ -0,0 +1,447 @@ +// Type definitions for evmlsdk 0.1.0 +// Project: https://github.com/mosaicnetworks/evml-client +// Definitions by: Mosaic Networks + +declare namespace EVMLClient { + + interface BaseTX { + gas?: number; + gasPrice?: number; + } + + interface BaseAccount { + address: string, + nonce: number, + balance: any + } + + interface TX extends BaseTX { + from: string; + to?: string; + value?: number; + data?: string; + } + + interface ContractOptions extends BaseTX { + from?: string; + address?: string; + data?: string; + jsonInterface: ABI[]; + } + + interface Input { + name: string; + type: string; + } + + interface ABI { + constant?: any; + inputs: Input[]; + name?: any; + outputs?: any[]; + payable: any; + stateMutability: any; + type: any; + } + + interface TXReceipt { + root: string; + transactionHash: string; + from: string; + to?: string; + gasUsed: number; + cumulativeGasUsed: number; + contractAddress: string; + logs: []; + logsBloom: string; + failed: boolean; + } + + interface SolidityCompilerOutput { + contracts: {}; + errors: string[]; + sourceList: string[]; + sources: {}; + } + + class Client { + readonly host: string; + readonly port: number; + private _constructOptions; + + constructor(host: string, port: number); + + getAccount(address: string): Promise; + + getAccounts(): Promise; + + testConnection(): Promise; + + getInfo(): Promise; + + call(tx: string): Promise; + + sendTx(tx: string): Promise; + + sendRawTx(tx: string): Promise; + + getReceipt(txHash: string): Promise; + } + + class Transaction { + readonly controller: Controller; + tx: TX; + receipt: TXReceipt; + readonly unpackfn: Function; + readonly constant: boolean; + + /** + * Transaction instance to be sent or called. + * + * @param {TX} options The transaction options eg. gas, gas price, value... + * @param {boolean} constant If the transaction is constant + * @param {Function} unpackfn If constant - unpack function + * @param {Controller} controller The controller class + */ + constructor(options: TX, constant: boolean, unpackfn: Function, controller: Controller); + + /** + * Send transaction. + * + * This function will mutate the state of the EVM. + * + * @param {Object} options + */ + send(options?: { + to?: string; + from?: string; + value?: number; + gas?: number; + gasPrice?: any; + }): any; + + /** + * Return transaction as string. + * + * @returns {string} Transaction as string + */ + toString(): string; + + /** + * Call transaction. + * + * This function will not mutate the state of the EVM. + * + * @param {Object} options + */ + call(options: { + to?: string; + from?: string; + value?: number; + gas?: number; + gasPrice?: any; + }): any; + + /** + * Sets the from of the transaction. + * + * @param {string} from The from address + * @returns {Transaction} The transaction + */ + from(from: string): Transaction; + + /** + * Sets the to of the transaction. + * + * @param {string} to The to address + * @returns {Transaction} The transaction + */ + to(to: string): Transaction; + + /** + * Sets the value of the transaction. + * + * @param {number} value The value of tx + * @returns {Transaction} The transaction + */ + value(value: number): Transaction; + + /** + * Sets the gas of the transaction. + * + * @param {number} gas The gas of tx + * @returns {Transaction} The transaction + */ + gas(gas: number): Transaction; + + /** + * Sets the gas price of the transaction. + * + * @param {number} gasPrice The gas price of tx + * @returns {Transaction} The transaction + */ + gasPrice(gasPrice: number): Transaction; + + /** + * Sets the data of the transaction. + * + * @param {number} data The data of tx + * @returns {Transaction} The transaction + */ + data(data: string): Transaction; + } + + class SolidityContract { + options: ContractOptions; + readonly controller: Controller; + methods: any; + web3Contract: any; + receipt: TXReceipt; + + /** + * Attaches functions to contract. + * + * Parses function data from ABI and creates Javascript instance representation then adds + * these functions to Contract.methods. + * + * @private + */ + private _attachMethodsToContract; + + /** + * Encodes constructor parameters. + * + * @param {Array} params The parameters to encode + * @private + */ + private _encodeConstructorParams; + + /** + * Javascript Object representation of a Solidity contract. + * + * Can either be used to deploy a contract or interact with a contract already deployed. + * + * @param {Controller} controller Controller Javascript instance + * @param {ContractOptions} options The options of the contract. eg. gas price, gas, address + * @constructor + */ + constructor(options: ContractOptions, controller: Controller); + + /** + * Deploy contract to the blockchain. + * + * Deploys contract to the blockchain and sets the newly acquired address of the contract. + * Also assigns the transaction receipt to this instance.. + * + * @param {Object} options The options for the contract. eg. constructor params, gas, gas price, data + * @returns {SolidityContract} Returns deployed contract with receipt and address attributes + */ + deploy(options?: { + parameters?: any[]; + gas?: number; + gasPrice?: any; + data?: string; + }): any; + + /** + * Sets the address of the contract and populates Solidity functions. + * + * @param {string} address The address to assign to the contract + * @returns {SolidityContract} The contract + */ + setAddressAndPopulate(address: string): SolidityContract; + + /** + * Sets the address of the contract. + * + * @param {string} address The address to assign to the contract + * @returns {SolidityContract} The contract + */ + address(address: string): SolidityContract; + + /** + * Sets the default gas for the contract. + * + * Any functions from the this contract will inherit the `gas` value by default. + * You still have the option to override the value once the transaction instance is instantiated. + * + * @param {number} gas The gas to assign to the contract + * @returns {SolidityContract} The contract + */ + gas(gas: number): SolidityContract; + + /** + * Sets the default gas price for the contract. + * + * Any functions from the this contract will inherit the `gasPrice` value by default. + * You still have the option to override the value once the transaction instance is instantiated. + * + * @param {number} gasPrice The gas price to assign to the contract + * @returns {SolidityContract} The contract + */ + gasPrice(gasPrice: number): SolidityContract; + + /** + * Sets the data for deploying the contract. + * + * @param {string} data The data of the contract + * @returns {SolidityContract} The contract + */ + data(data: string): SolidityContract; + + /** + * Sets the JSON Interface of the contract. + * + * @param {ABI[]} abis The JSON Interface of contract + * @returns {SolidityContract} The contract + */ + JSONInterface(abis: ABI[]): SolidityContract; + } + + interface DefaultTXOptions extends BaseTX { + from?: string, + } + + export class Controller { + readonly host: string; + readonly port: number; + defaultAddress: string; + accounts: Account[]; + defaultGas: number; + defaultGasPrice: number; + defaultFrom: string; + readonly api: Client; + private _defaultTXOptions: DefaultTXOptions; + + /** + * Creates a controller instance. + * + * This class controls all of functionality for interacting with an EVM-Lite node. + * + * @param {string} host The IP or alias of the EVM-Lite node + * @param {number} port Port to access the service. default = 8080 + * @constructor + */ + constructor(host: string, port?: number); + + /** + * Generates Javascript instance from Solidity Contract File. + * + * Takes a solidity file and generates corresponding functions associated with the contract + * name provided. The byte-code of the contract is auto-assigned to the data option field + * for the contract. + * + * @param {string} contractName Name of the Contract to get from Solidity file + * @param {string} filePath Absolute or relative path of the Solidity file. + * @returns {SolidityContract} A Javascript instance representation of solidity contract + */ + ContractFromSolidityFile(contractName: string, filePath: string): SolidityContract; + + /** + * Generates Contract Javascript instance from Solidity Contract File. + * + * Takes ABI and generates corresponding functions associated with the contract provided. + * The byte-code of the contract needs to be assigned before deploying. Mostly used to + * interact with already deployed contracts. + * + * @param {ABI[]} abi The Application Binary Interface of the Solidity contract + * @returns {SolidityContract} A Javascript instance representation of solidity contract + */ + ContractFromABI(abi: ABI[]): SolidityContract; + + /** + * Transfer a specified value to the desired address. + * + * Sender address can be set after instantiating the Controller instance (recommended) or + * after the Transaction instance has been created. + * + * @param {string} to - The address of the sender + * @param {string} from - The address of the receiver + * @param {number} value - The value to send the receiver + * @returns {Transaction} the required Transaction instance for transfer request + */ + transfer(to: string, from: string, value: number): Transaction; + + /** + * Require default from address to be set. + * + * @private + */ + private _requireDefaultFromAddress(): void; + + } + + export class Utils { + static fgRed: string; + static fgGreen: string; + static fgBlue: string; + static fgMagenta: string; + static fgCyan: string; + static fgWhite: string; + static fgOrange: string; + + static log(color: string, text: string); + + static step(message: string); + + static explain(message: string); + + static space(); + + static sleep(time: number); + } + + interface Web3Account { + address: string, + privateKey: string, + sign: (data: string) => any, + encrypt: (password: string) => any, + signTransaction: (tx: string) => any, + } + + interface KDFEncryption { + ciphertext: string, + ciperparams: { + iv: string + } + cipher: string, + kdf: string, + kdfparams: { + dklen: number, + salt: string, + n: number, + r: number, + p: number + } + mac: string + } + + interface v3JSONKeyStore { + version: number, + id: string, + address: string, + crypto: KDFEncryption, + } + + export class Account { + + address: string; + balance: any; + nonce: number; + privateKey: string; + sign: (data: string) => any; + private _account: Web3Account; + + static create(): Account; + + static decrypt(v3JSONKeyStore: v3JSONKeyStore, password: string): Account; + + signTransaction(tx: string): any; + + encrypt(password: string): v3JSONKeyStore; + + toBaseAccount(): BaseAccount; + } +} + +export = EVMLClient diff --git a/lib/@types/index.test-d..ts b/lib/@types/index.test-d..ts new file mode 100644 index 0000000..e69de29 diff --git a/lib/CHANGELOG.md b/lib/CHANGELOG.md new file mode 100644 index 0000000..933cf8a --- /dev/null +++ b/lib/CHANGELOG.md @@ -0,0 +1,27 @@ +## UNRELEASED + +FEATURES: + +* call() and send() transactions +* API to handle interactions with transactions +* Contract abstraction + * Solidity function abstraction +* Transfer tokens +* Promised wrapped HTTP handler +* Default options for top level class +* Account handling (WiP) +* Wallet handling (WiP) + +IMPROVEMENTS: + +* Testing Capabilities +* Documentation for classes and functions +* Functions to set transaction attributes +* User experience improvements SolidityContract.ts +* User experience improvements SolidityFunction.ts +* Type definitions + + +SECURITY: + +BUG FIXES: \ No newline at end of file diff --git a/lib/dist/Controller.js b/lib/dist/Controller.js new file mode 100644 index 0000000..df17850 --- /dev/null +++ b/lib/dist/Controller.js @@ -0,0 +1,169 @@ +"use strict"; +/** + * @file Controller.js + * @author Mosaic Networks + * @date 2018 + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const JSONBig = require("json-bigint"); +const fs = require("fs"); +const solidityCompiler = require("solc"); +const SolidityContract_1 = require("./evm/SolidityContract"); +const Client_1 = require("./evm/Client"); +const Transaction_1 = require("./evm/Transaction"); +/** + * The root class to interactive with EVM-Lite. + */ +class Controller { + /** + * Creates a controller instance. + * + * This class controls all of functionality for interacting with an EVM-Lite node. + * + * @param {string} host - The IP or alias of the EVM-Lite node + * @param {number} port - Port to access the service. default = 8080 + * @param {DefaultTXOptions} _defaultTXOptions - Default transaction options + * @constructor + */ + constructor(host, port = 8080, _defaultTXOptions = {}) { + this.host = host; + this.port = port; + this._defaultTXOptions = _defaultTXOptions; + this.accounts = []; + this.api = new Client_1.default(host, port); + } + /** + * Return default options + * + * @returns {DefaultTXOptions} A Javascript instance with default transaction parameters + */ + get defaultOptions() { + return this._defaultTXOptions; + } + /** + * Get default `from` address + * + * @returns {string} The default `from` address + */ + get defaultFrom() { + return this._defaultTXOptions.from; + } + /** + * Set default `from` address + * + * @param {string} address - The address to set default `from` value + */ + set defaultFrom(address) { + this._defaultTXOptions.from = address; + } + /** + * Get default `gas` value + * + * @returns {number} The default `gas` value + */ + get defaultGas() { + return this._defaultTXOptions.gas; + } + /** + * Set default `gas` value + * + * @param {number} gas - The gas value to set as default + */ + set defaultGas(gas) { + this._defaultTXOptions.gas = gas; + } + /** + * Get default `gasPrice` value + * + * @returns {number} The default `gasPrice` value + */ + get defaultGasPrice() { + return this._defaultTXOptions.gasPrice; + } + /** + * Set default `from` address + * + * @param {number} gasPrice - The gasPrice value to set as default + */ + set defaultGasPrice(gasPrice) { + this._defaultTXOptions.gasPrice = gasPrice; + } + /** + * Generates Javascript instance from Solidity Contract File. + * + * Takes a solidity file and generates corresponding functions associated with the contract + * name provided. The byte-code of the contract is auto-assigned to the data option field + * for contract deployment. + * + * @param {string} contractName - Name of the Contract to get from Solidity file + * @param {string} filePath - Absolute or relative path of the Solidity file. + * @returns {SolidityContract} A Javascript instance representation of solidity contract + */ + ContractFromSolidityFile(contractName, filePath) { + this._requireDefaultFromAddress(); + let input = fs.readFileSync(filePath).toString(); + let output = solidityCompiler.compile(input, 1); + let byteCode = output.contracts[`:${contractName}`].bytecode; + let abi = JSONBig.parse(output.contracts[`:${contractName}`].interface); + return new SolidityContract_1.default({ + jsonInterface: abi, + data: byteCode, + gas: this._defaultTXOptions.gas || undefined, + gasPrice: this._defaultTXOptions.gasPrice || undefined + }, this); + } + ; + /** + * Generates Contract Javascript instance from Solidity Contract File. + * + * Takes ABI and generates corresponding functions associated with the contract provided. + * The byte-code of the contract needs to be assigned before deploying. Mostly used to + * interact with already deployed contracts. + * + * @param {ABI[]} abi - The Application Binary Interface of the Solidity contract + * @returns {SolidityContract} A Javascript instance representation of solidity contract + */ + ContractFromABI(abi) { + this._requireDefaultFromAddress(); + return new SolidityContract_1.default({ + jsonInterface: abi, + gas: this._defaultTXOptions.gas || undefined, + gasPrice: this._defaultTXOptions.gasPrice || undefined + }, this); + } + /** + * Transfer a specified value to the desired address. + * + * Sender address can be set after instantiating the Controller instance (recommended) or + * after the Transaction instance has been created. + * + * @param {string} to - The address of the sender + * @param {string} from - The address of the receiver + * @param {number} value - The value to send the receiver + * @returns {Transaction} the required Transaction instance for transfer request + */ + transfer(to, from, value) { + if (from === '') { + from = this.defaultOptions.from; + } + return new Transaction_1.default({ + from: from, + to: to, + value: value, + gas: this._defaultTXOptions.gas || undefined, + gasPrice: this._defaultTXOptions.gasPrice || undefined + }, false, undefined, this); + } + /** + * Require default from address to be set. + * + * @private + */ + _requireDefaultFromAddress() { + if (this._defaultTXOptions.from == null) { + throw new Error('Set default `from` address. use `EVML.defaultFrom(
)`'); + } + } + ; +} +exports.default = Controller; diff --git a/lib/dist/evm/Account.js b/lib/dist/evm/Account.js new file mode 100644 index 0000000..3681f7b --- /dev/null +++ b/lib/dist/evm/Account.js @@ -0,0 +1,54 @@ +"use strict"; +/** + * @file Account.js + * @author Mosaic Networks + * @date 2018 + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const Web3Accounts = require("web3-eth-accounts"); +class Account { + constructor(create = true, aJSON = undefined) { + this.balance = 0; + this.nonce = 0; + if (create) + this._account = new Web3Accounts().create(); + else { + if (aJSON) { + this._account = aJSON; + } + else { + throw new Error('Account JSON needs to be passed to construct class'); + } + } + } + get sign() { + return this._account.sign; + } + signTransaction(tx) { + return this._account.signTransaction(tx); + } + get address() { + return this._account.address; + } + get privateKey() { + return this._account.privateKey; + } + static create() { + return new Account(true); + } + static decrypt(v3JSONKeyStore, password) { + let decryptedAccount = new Web3Accounts().decrypt(v3JSONKeyStore, password); + return new Account(false, decryptedAccount); + } + encrypt(password) { + return this._account.encrypt(password); + } + toBaseAccount() { + return { + address: this.address, + balance: this.balance, + nonce: this.nonce + }; + } +} +exports.default = Account; diff --git a/lib/dist/evm/Client.js b/lib/dist/evm/Client.js new file mode 100644 index 0000000..69ee651 --- /dev/null +++ b/lib/dist/evm/Client.js @@ -0,0 +1,102 @@ +"use strict"; +/** + * @file Client.js + * @author Mosaic Networks + * @date 2018 + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const http = require("http"); +const JSONBig = require("json-bigint"); +const Chalk = require("chalk"); +const error = (message) => { + console.log(Chalk.default.red(message)); +}; +let request = (tx, options) => { + return new Promise((resolve, reject) => { + const req = http.request(options, (response) => { + let data = ''; + response.on('data', (chunk) => data += chunk); + response.on('end', () => resolve(data)); + response.on('error', (err) => reject(err)); + }); + req.on('error', (err) => { + reject(err); + }); + if (tx) + req.write(tx); + req.end(); + }); +}; +class Client { + constructor(host, port) { + this.host = host; + this.port = port; + } + getAccount(address) { + return request(null, this._constructOptions('GET', `/account/${address}`)) + .then((response) => { + let account = JSONBig.parse(response); + if (typeof account.balance === 'object') { + account.balance = account.balance.toFormat(0); + } + return account; + }) + .catch(() => error('Could not fetch account.')); + } + testConnection() { + return request(null, this._constructOptions('GET', '/info')) + .then(() => true) + .catch(() => error('Could connect to node.')); + } + getAccounts() { + return request(null, this._constructOptions('GET', '/accounts')) + .then((response) => { + let json = JSONBig.parse(response); + if (json.accounts) { + return json.accounts.map((account) => { + if (typeof account.balance === 'object') { + account.balance = account.balance.toFormat(0); + } + return account; + }); + } + else { + return []; + } + }) + .catch(() => error('Could not fetch accounts.')); + } + getInfo() { + return request(null, this._constructOptions('GET', '/info')) + .then((response) => JSONBig.parse(response)) + .catch(() => error('Could not fetch information.')); + } + call(tx) { + return request(tx, this._constructOptions('POST', '/call')) + .then((response) => response) + .catch(err => error(err)); + } + sendTx(tx) { + return request(tx, this._constructOptions('POST', '/tx')) + .then((response) => response) + .catch(err => error(err)); + } + sendRawTx(tx) { + return request(tx, this._constructOptions('POST', '/rawtx')) + .then((response) => response); + } + getReceipt(txHash) { + return request(null, this._constructOptions('GET', `/tx/${txHash}`)) + .then((response) => JSONBig.parse(response)) + .catch(() => error(`Could not fetch receipt for hash: ${txHash}`)); + } + _constructOptions(method, path) { + return { + host: this.host, + port: this.port, + method: method, + path: path + }; + } +} +exports.default = Client; diff --git a/lib/dist/evm/SolidityContract.js b/lib/dist/evm/SolidityContract.js new file mode 100644 index 0000000..d6116c7 --- /dev/null +++ b/lib/dist/evm/SolidityContract.js @@ -0,0 +1,188 @@ +"use strict"; +/** + * @file SolidityContract.js + * @author Mosaic Networks + * @date 2018 + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const Web3 = require("web3"); +const coder = require("web3/lib/solidity/coder.js"); +const errors = require("./utils/errors"); +const checks = require("./utils/checks"); +const SolidityFunction_1 = require("./SolidityFunction"); +const Transaction_1 = require("./Transaction"); +class SolidityContract { + /** + * Javascript Object representation of a Solidity contract. + * + * Can either be used to deploy a contract or interact with a contract already deployed. + * + * @param {Controller} controller - Controller Javascript instance + * @param {ContractOptions} options - The options of the contract. eg. gas price, gas, address + * @constructor + */ + constructor(options, controller) { + this.options = options; + this.controller = controller; + const web3 = new Web3(); + this.options.address = options.address || ''; + this.web3Contract = web3.eth.contract(this.options.jsonInterface).at(this.options.address); + this.receipt = undefined; + this.methods = {}; + if (this.options.address !== undefined) + this._attachMethodsToContract(); + } + /** + * Deploy contract to the blockchain. + * + * Deploys contract to the blockchain and sets the newly acquired address of the contract. + * Also assigns the transaction receipt to this instance. + * + * @param {Object} options - The options for the contract. eg. constructor params, gas, gas price, data + * @returns {SolidityContract} Returns deployed contract with receipt and address attributes + */ + deploy(options) { + if (this.options.address !== '') + throw errors.ContractAddressFieldSetAndDeployed(); + this.options.jsonInterface.filter((abi) => { + if (abi.type === "constructor" && options.parameters) { + checks.requireArgsLength(abi.inputs.length, options.parameters.length); + } + }); + if (options) { + this.options.data = options.data || this.options.data; + this.options.gas = options.gas || this.options.gas; + this.options.gasPrice = options.gasPrice || this.options.gasPrice; + } + if (this.options.data) { + let encodedData; + if (options.parameters) + encodedData = this.options.data + this._encodeConstructorParams(options.parameters); + return new Transaction_1.default({ + from: this.controller.defaultOptions.from, + data: encodedData + }, false, undefined, this.controller) + .gas(this.options.gas) + .gasPrice(this.options.gasPrice) + .send().then((receipt) => { + this.receipt = receipt; + return this.setAddressAndPopulate(this.receipt.contractAddress); + }); + } + else { + throw errors.InvalidDataFieldInOptions(); + } + } + /** + * Sets the address of the contract and populates Solidity contract functions. + * + * @param {string} address - The address to assign to the contract + * @returns {SolidityContract} The contract + */ + setAddressAndPopulate(address) { + this.options.address = address; + this._attachMethodsToContract(); + return this; + } + /** + * Sets the address of the contract. + * + * @param {string} address - The address to assign to the contract + * @returns {SolidityContract} The contract + */ + address(address) { + this.options.address = address; + return this; + } + /** + * Sets the default gas for the contract. + * + * Any functions from the this contract will inherit the `gas` value by default. + * You still have the option to override the value once the transaction instance is instantiated. + * + * @param {number} gas - The gas to assign to the contract + * @returns {SolidityContract} The contract + */ + gas(gas) { + this.options.gas = gas; + return this; + } + /** + * Sets the default gas price for the contract. + * + * Any functions from the this contract will inherit the `gasPrice` value by default. + * You still have the option to override the value once the transaction instance is instantiated. + * + * @param {number} gasPrice - The gas price to assign to the contract + * @returns {SolidityContract} The contract + */ + gasPrice(gasPrice) { + this.options.gasPrice = gasPrice; + return this; + } + /** + * Sets the data for deploying the contract. + * + * @param {string} data - The data of the contract + * @returns {SolidityContract} The contract + */ + data(data) { + this.options.data = data; + return this; + } + /** + * Sets the JSON Interface of the contract. + * + * @param {ABI[]} abis - The JSON Interface of contract + * @returns {SolidityContract} The contract + */ + JSONInterface(abis) { + this.options.jsonInterface = abis; + return this; + } + /** + * Attaches functions to contract. + * + * Parses function data from ABI and creates Javascript instance representation then adds + * these functions to Contract.methods. + * + * @private + */ + _attachMethodsToContract() { + this.options.jsonInterface.filter((json) => { + return json.type === 'function'; + }) + .map((funcJSON) => { + let solFunction = new SolidityFunction_1.default(funcJSON, this.options.address, this.controller); + if (this.options.gas !== undefined && this.options.gasPrice !== undefined) { + this.methods[funcJSON.name] = solFunction.generateTransaction.bind(solFunction, { + gas: this.options.gas, + gasPrice: this.options.gasPrice, + }); + } + else { + this.methods[funcJSON.name] = solFunction.generateTransaction.bind(solFunction, {}); + } + }); + } + /** + * Encodes constructor parameters. + * + * @param {Array} params - The parameters to encode + * @private + */ + _encodeConstructorParams(params) { + return this.options.jsonInterface.filter((json) => { + return json.type === 'constructor' && json.inputs.length === params.length; + }) + .map((json) => { + return json.inputs.map((input) => { + return input.type; + }); + }) + .map((types) => { + return coder.encodeParams(types, params); + })[0] || ''; + } +} +exports.default = SolidityContract; diff --git a/lib/dist/evm/SolidityFunction.js b/lib/dist/evm/SolidityFunction.js new file mode 100644 index 0000000..62003e6 --- /dev/null +++ b/lib/dist/evm/SolidityFunction.js @@ -0,0 +1,89 @@ +"use strict"; +/** + * @file SolidityFunction.js + * @author Mosaic Networks + * @date 2018 + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const SolFunction = require("web3/lib/web3/function.js"); +const coder = require("web3/lib/solidity/coder.js"); +const checks = require("./utils/checks"); +const Transaction_1 = require("./Transaction"); +class SolidityFunction { + /** + * Javascript Object representation of a Solidity function. + * + * @param {ABI} abi - JSON describing the function details + * @param {string} contractAddress - The address of parent contract + * @param {Controller} controller - The controller class + * @constructor + */ + constructor(abi, contractAddress, controller) { + this.contractAddress = contractAddress; + this.controller = controller; + this.name = abi.name; + this._solFunction = new SolFunction('', abi, ''); + this._constant = (abi.stateMutability === "view" || abi.stateMutability === "pure" || abi.constant); + this._payable = (abi.stateMutability === "payable" || abi.payable); + this._inputTypes = abi.inputs.map((i) => { + return i.type; + }); + this._outputTypes = abi.outputs.map((i) => { + return i.type; + }); + } + /** + * Generates Transaction instance to be sent or called. + * + * Creates the scaffolding needed for the transaction to be executed. + * + * @param {Object} options - The options for the transaction of this function + * @param {Array} funcArgs - A list containing all the parameters of the function + */ + generateTransaction(options, ...funcArgs) { + this._validateArgs(funcArgs); + let callData = this._solFunction.getData(); + let tx = { + from: this.controller.defaultOptions.from, + to: this.contractAddress, + }; + if (options && options.gas !== undefined && options.gasPrice !== undefined) { + tx.gas = options.gas; + tx.gasPrice = options.gasPrice; + } + tx.data = callData; + if (tx.value <= 0 && this._payable) + throw Error('Function is payable and requires `value` greater than 0.'); + else if (tx.value > 0 && !this._payable) + throw Error('Function is not payable. Required `value` is 0.'); + let unpackfn = undefined; + if (this._constant) + unpackfn = this.unpackOutput.bind(this); + return new Transaction_1.default(tx, this._constant, unpackfn, this.controller); + } + /** + * Decodes output with the corresponding return types. + * + * @param {string} output - The output string to decode + */ + unpackOutput(output) { + output = output.length >= 2 ? output.slice(2) : output; + let result = coder.decodeParams(this._outputTypes, output); + return result.length === 1 ? result[0] : result; + } + /** + * Validates arguments to the function. + * + * This checks types as well as length of input arguments to required. + * + * @param {Array} args - The list of arguments for the function + * @private + */ + _validateArgs(args) { + checks.requireArgsLength(this._inputTypes.length, args.length); + args.map((a, i) => { + checks.requireSolidityTypes(this._inputTypes[i], a); + }); + } +} +exports.default = SolidityFunction; diff --git a/lib/dist/evm/Transaction.js b/lib/dist/evm/Transaction.js new file mode 100644 index 0000000..36585ed --- /dev/null +++ b/lib/dist/evm/Transaction.js @@ -0,0 +1,174 @@ +"use strict"; +/** + * @file Transaction.js + * @author Mosaic Networks + * @date 2018 + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const JSONBig = require("json-bigint"); +class Transaction { + /** + * Transaction instance to be sent or called. + * + * @param {TX} _tx - The transaction options eg. gas, gas price, value... + * @param {boolean} constant - If the transaction is constant + * @param {Function} unpackfn - If constant unpack function + * @param {Controller} controller - The controller class + */ + constructor(_tx, constant, unpackfn, controller) { + this._tx = _tx; + this.constant = constant; + this.unpackfn = unpackfn; + this.controller = controller; + this.receipt = undefined; + if (!constant) + this.unpackfn = undefined; + } + /** + * Send transaction. + * + * This function will mutate the state of the EVM. + * + * @param {Object} options - The options to pass to the transaction + */ + send(options) { + if (!this.constant) { + if (options) { + this._tx.to = options.to || this._tx.to; + this._tx.from = options.from || this._tx.from; + this._tx.gas = options.gas || this._tx.gas; + this._tx.value = options.value || this._tx.value; + if (options.gasPrice !== undefined && options.gasPrice >= 0) { + this._tx.gasPrice = options.gasPrice; + } + } + if (this._tx.gas != null && this._tx.gasPrice != null) { + return this.controller.api.sendTx(JSONBig.stringify(this._tx)) + .then((res) => { + let response = JSONBig.parse(res); + return response.txHash; + }) + .then((txHash) => { + return new Promise((resolve) => setTimeout(resolve, 2000)) + .then(() => { + return this.controller.api.getReceipt(txHash); + }); + }) + .then((resp) => { + this.receipt = JSONBig.parse(resp); + return this.receipt; + }); + } + else { + throw new Error('gas & gas price not set'); + } + } + else { + throw new Error('Transaction does not mutate state. Use `call()` instead'); + } + } + /** + * Call transaction. + * + * This function will not mutate the state of the EVM. + * + * @param {Object} options - The options to pass to the transaction + */ + call(options) { + if (this.constant) { + if (options) { + this._tx.to = options.to || this._tx.to; + this._tx.from = options.from || this._tx.from; + this._tx.gas = options.gas || this._tx.gas; + this._tx.value = options.value || this._tx.value; + if (options.gasPrice !== undefined && options.gasPrice >= 0) { + this._tx.gasPrice = options.gasPrice; + } + } + if (this._tx.gas != null && this._tx.gasPrice != null) { + return this.controller.api.call(JSONBig.stringify(this._tx)) + .then((response) => { + return JSONBig.parse(response); + }) + .then((obj) => { + return this.unpackfn(Buffer.from(obj.data).toString()); + }); + } + else { + throw new Error('gas & gas price not set'); + } + } + else { + throw new Error('Transaction mutates state. Use `send()` instead'); + } + } + /** + * Return transaction as string. + * + * @returns {string} Transaction as string + */ + toString() { + return JSONBig.stringify(this._tx); + } + /** + * Sets the from of the transaction. + * + * @param {string} from - The from address + * @returns {Transaction} The transaction + */ + from(from) { + this._tx.from = from; + return this; + } + /** + * Sets the to of the transaction. + * + * @param {string} to - The to address + * @returns {Transaction} The transaction + */ + to(to) { + this._tx.to = to; + return this; + } + /** + * Sets the value of the transaction. + * + * @param {number} value - The value of tx + * @returns {Transaction} The transaction + */ + value(value) { + this._tx.value = value; + return this; + } + /** + * Sets the gas of the transaction. + * + * @param {number} gas - The gas of tx + * @returns {Transaction} The transaction + */ + gas(gas) { + this._tx.gas = gas; + return this; + } + /** + * Sets the gas price of the transaction. + * + * @param {number} gasPrice - The gas price of tx + * @returns {Transaction} The transaction + */ + gasPrice(gasPrice) { + this._tx.gasPrice = gasPrice; + return this; + } + /** + * Sets the data of the transaction. + * + * @param {string} data - The data of tx + * @returns {Transaction} The transaction + */ + data(data) { + this._tx.data = data; + return this; + } +} +exports.default = Transaction; diff --git a/lib/dist/evm/Wallet.js b/lib/dist/evm/Wallet.js new file mode 100644 index 0000000..3ad3f1b --- /dev/null +++ b/lib/dist/evm/Wallet.js @@ -0,0 +1,19 @@ +/** + * @file Wallet.js + * @author Mosaic Networks + * @date 2018 + */ +class Wallet { + constructor() { + } + add() { + } + remove() { + } + clear() { + } + encrypt() { + } + decrypt() { + } +} diff --git a/lib/dist/evm/utils/Interfaces.js b/lib/dist/evm/utils/Interfaces.js new file mode 100644 index 0000000..9df0df0 --- /dev/null +++ b/lib/dist/evm/utils/Interfaces.js @@ -0,0 +1,7 @@ +"use strict"; +/** + * @file Interface.js + * @author Mosaic Networks + * @date 2018 + */ +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/dist/evm/utils/checks.js b/lib/dist/evm/utils/checks.js new file mode 100644 index 0000000..ed09322 --- /dev/null +++ b/lib/dist/evm/utils/checks.js @@ -0,0 +1,34 @@ +"use strict"; +/** + * @file checks.js + * @author Mosaic Networks + * @date 2018 + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const errors = require("./errors"); +exports.requireArgsLength = (expected, received) => { + if (expected !== received) { + throw errors.InvalidNumberOfSolidityArgs(expected, received); + } + else { + return true; + } +}; +exports.requireSolidityTypes = (required, received) => { + if (typeof received !== parseSolidityType(required)) { + throw errors.InvalidSolidityType(); + } + else { + return true; + } +}; +const parseSolidityType = (type) => { + switch (type.toLowerCase()) { + case 'address': + return 'string'; + } + if (type.toLowerCase().includes('int')) { + return 'number'; + } + return undefined; +}; diff --git a/lib/dist/evm/utils/errors.js b/lib/dist/evm/utils/errors.js new file mode 100644 index 0000000..bb1392a --- /dev/null +++ b/lib/dist/evm/utils/errors.js @@ -0,0 +1,23 @@ +"use strict"; +/** + * @file errors.js + * @author Mosaic Networks + * @date 2018 + */ +Object.defineProperty(exports, "__esModule", { value: true }); +function InvalidNumberOfSolidityArgs(expected, received) { + return new Error(`Expected ${expected} but got ${received} arguments.`); +} +exports.InvalidNumberOfSolidityArgs = InvalidNumberOfSolidityArgs; +function InvalidSolidityType() { + return new TypeError('Invalid argument type'); +} +exports.InvalidSolidityType = InvalidSolidityType; +function InvalidDataFieldInOptions() { + return new Error('`data` field must be specified before deploying contract.'); +} +exports.InvalidDataFieldInOptions = InvalidDataFieldInOptions; +function ContractAddressFieldSetAndDeployed() { + return new Error('Contract\'s address option is already set. Please reset to undefined to deploy.'); +} +exports.ContractAddressFieldSetAndDeployed = ContractAddressFieldSetAndDeployed; diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..22ad0cf --- /dev/null +++ b/lib/index.js @@ -0,0 +1,2 @@ +exports.Controller = require('./dist/Controller').default; +exports.Account = require('./dist/evm/Account').default; \ No newline at end of file diff --git a/lib/package.json b/lib/package.json new file mode 100644 index 0000000..2bcf2a8 --- /dev/null +++ b/lib/package.json @@ -0,0 +1,30 @@ +{ + "name": "evml-client", + "version": "0.1.0", + "description": "A Node.js module for interacting with EVM-Lite.", + "main": "index.js", + "scripts": { + "test": "mocha" + }, + "author": "Danu Kumanan", + "license": "MIT", + "dependencies": { + "json-bigint": "^0.3.0", + "solc": "^0.4.24", + "web3": "github:ethereum/web3.js", + "web3-eth-accounts": "^1.0.0-beta.26", + "chalk": "latest" + }, + "devDependencies": { + "@types/node": "^10.7.0", + "chai": "^4.1.2", + "jsdoc": "^3.5.5", + "mocha": "^5.2.0", + "ts-node": "^7.0.1" + }, + "typings": "@types/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/mosaicnetworks/evm-lite-nodejs-sdk" + } +} diff --git a/lib/src/Controller.ts b/lib/src/Controller.ts new file mode 100644 index 0000000..c6a5668 --- /dev/null +++ b/lib/src/Controller.ts @@ -0,0 +1,192 @@ +/** + * @file Controller.js + * @author Mosaic Networks + * @date 2018 + */ + +import * as JSONBig from 'json-bigint' +import * as fs from "fs"; +import * as solidityCompiler from 'solc' + +import {ABI, BaseAccount, BaseTX, SolidityCompilerOutput, TXReceipt} from "./evm/utils/Interfaces"; + +import SolidityContract from "./evm/SolidityContract"; +import Client from "./evm/Client"; +import Transaction from "./evm/Transaction"; +import Account from "./evm/Account" + +interface DefaultTXOptions extends BaseTX { + from?: string, +} + +/** + * The root class to interactive with EVM-Lite. + */ +export default class Controller { + + public accounts: Account[]; + readonly api: Client; + + /** + * Creates a controller instance. + * + * This class controls all of functionality for interacting with an EVM-Lite node. + * + * @param {string} host - The IP or alias of the EVM-Lite node + * @param {number} port - Port to access the service. default = 8080 + * @param {DefaultTXOptions} _defaultTXOptions - Default transaction options + * @constructor + */ + constructor(readonly host: string, readonly port: number = 8080, private _defaultTXOptions: DefaultTXOptions = {}) { + this.accounts = []; + this.api = new Client(host, port); + } + + /** + * Return default options + * + * @returns {DefaultTXOptions} A Javascript instance with default transaction parameters + */ + get defaultOptions(): DefaultTXOptions { + return this._defaultTXOptions; + } + + /** + * Get default `from` address + * + * @returns {string} The default `from` address + */ + get defaultFrom(): string { + return this._defaultTXOptions.from; + } + + /** + * Set default `from` address + * + * @param {string} address - The address to set default `from` value + */ + set defaultFrom(address: string) { + this._defaultTXOptions.from = address; + } + + /** + * Get default `gas` value + * + * @returns {number} The default `gas` value + */ + get defaultGas(): number { + return this._defaultTXOptions.gas; + } + + /** + * Set default `gas` value + * + * @param {number} gas - The gas value to set as default + */ + set defaultGas(gas: number) { + this._defaultTXOptions.gas = gas; + } + + /** + * Get default `gasPrice` value + * + * @returns {number} The default `gasPrice` value + */ + get defaultGasPrice(): number { + return this._defaultTXOptions.gasPrice; + } + + /** + * Set default `from` address + * + * @param {number} gasPrice - The gasPrice value to set as default + */ + set defaultGasPrice(gasPrice: number) { + this._defaultTXOptions.gasPrice = gasPrice; + } + + /** + * Generates Javascript instance from Solidity Contract File. + * + * Takes a solidity file and generates corresponding functions associated with the contract + * name provided. The byte-code of the contract is auto-assigned to the data option field + * for contract deployment. + * + * @param {string} contractName - Name of the Contract to get from Solidity file + * @param {string} filePath - Absolute or relative path of the Solidity file. + * @returns {SolidityContract} A Javascript instance representation of solidity contract + */ + ContractFromSolidityFile(contractName: string, filePath: string): SolidityContract { + this._requireDefaultFromAddress(); + + let input = fs.readFileSync(filePath).toString(); + let output: SolidityCompilerOutput = solidityCompiler.compile(input, 1); + let byteCode = output.contracts[`:${contractName}`].bytecode; + let abi = JSONBig.parse(output.contracts[`:${contractName}`].interface); + + return new SolidityContract({ + jsonInterface: abi, + data: byteCode, + gas: this._defaultTXOptions.gas || undefined, + gasPrice: this._defaultTXOptions.gasPrice || undefined + }, this) + }; + + /** + * Generates Contract Javascript instance from Solidity Contract File. + * + * Takes ABI and generates corresponding functions associated with the contract provided. + * The byte-code of the contract needs to be assigned before deploying. Mostly used to + * interact with already deployed contracts. + * + * @param {ABI[]} abi - The Application Binary Interface of the Solidity contract + * @returns {SolidityContract} A Javascript instance representation of solidity contract + */ + ContractFromABI(abi: ABI[]): SolidityContract { + this._requireDefaultFromAddress(); + + return new SolidityContract({ + jsonInterface: abi, + gas: this._defaultTXOptions.gas || undefined, + gasPrice: this._defaultTXOptions.gasPrice || undefined + }, this); + } + + /** + * Transfer a specified value to the desired address. + * + * Sender address can be set after instantiating the Controller instance (recommended) or + * after the Transaction instance has been created. + * + * @param {string} to - The address of the sender + * @param {string} from - The address of the receiver + * @param {number} value - The value to send the receiver + * @returns {Transaction} the required Transaction instance for transfer request + */ + transfer(to: string, from: string, value: number): Transaction { + if (from === '') { + from = this.defaultOptions.from; + } + + return new Transaction({ + from: from, + to: to, + value: value, + gas: this._defaultTXOptions.gas || undefined, + gasPrice: this._defaultTXOptions.gasPrice || undefined + }, false, undefined, this) + } + + + /** + * Require default from address to be set. + * + * @private + */ + private _requireDefaultFromAddress(): void { + if (this._defaultTXOptions.from == null) { + throw new Error('Set default `from` address. use `EVML.defaultFrom(
)`'); + } + }; + +} \ No newline at end of file diff --git a/lib/src/evm/Account.ts b/lib/src/evm/Account.ts new file mode 100644 index 0000000..112fbfe --- /dev/null +++ b/lib/src/evm/Account.ts @@ -0,0 +1,100 @@ +/** + * @file Account.js + * @author Mosaic Networks + * @date 2018 + */ + +import * as Web3Accounts from 'web3-eth-accounts'; +import {BaseAccount} from "./utils/Interfaces"; + + +interface Web3Account { + address: string, + privateKey: string, + sign: (data: string) => any, + encrypt: (password: string) => v3JSONKeyStore, + signTransaction: (tx: string) => any, +} + +interface KDFEncryption { + ciphertext: string, + ciperparams: { + iv: string + } + cipher: string, + kdf: string, + kdfparams: { + dklen: number, + salt: string, + n: number, + r: number, + p: number + } + mac: string +} + +interface v3JSONKeyStore { + version: number, + id: string, + address: string, + crypto: KDFEncryption, +} + + +export default class Account { + + balance: number = 0; + nonce: number = 0; + private _account: Web3Account; + + + constructor(create: boolean = true, aJSON: Web3Account = undefined) { + if (create) + this._account = new Web3Accounts().create(); + else { + if (aJSON) { + this._account = aJSON; + } else { + throw new Error('Account JSON needs to be passed to construct class'); + } + } + } + + get sign(): (data: string) => any { + return this._account.sign + } + + signTransaction(tx: string): any { + return this._account.signTransaction(tx); + } + + get address(): string { + return this._account.address + } + + get privateKey(): string { + return this._account.privateKey + } + + static create(): Account { + return new Account(true) + } + + static decrypt(v3JSONKeyStore: v3JSONKeyStore, password: string) { + let decryptedAccount = new Web3Accounts().decrypt(v3JSONKeyStore, password); + return new Account(false, decryptedAccount); + } + + encrypt(password: string): v3JSONKeyStore { + return this._account.encrypt(password); + } + + toBaseAccount(): BaseAccount { + return { + address: this.address, + balance: this.balance, + nonce: this.nonce + } + } + +} \ No newline at end of file diff --git a/lib/src/evm/Client.ts b/lib/src/evm/Client.ts new file mode 100644 index 0000000..19f9e31 --- /dev/null +++ b/lib/src/evm/Client.ts @@ -0,0 +1,114 @@ +/** + * @file Client.js + * @author Mosaic Networks + * @date 2018 + */ + +import * as http from 'http' +import * as JSONBig from 'json-bigint' +import * as Chalk from "chalk"; + +import {BaseAccount, TXReceipt} from "./utils/Interfaces"; + + +const error = (message: any): void => { + console.log(Chalk.default.red(message)); +}; + +let request = (tx, options): Promise => { + return new Promise((resolve, reject) => { + const req = http.request(options, (response) => { + let data = ''; + response.on('data', (chunk) => data += chunk); + response.on('end', () => resolve(data)); + response.on('error', (err) => reject(err)); + }); + + req.on('error', (err) => { + reject(err); + }); + + if (tx) req.write(tx); + + req.end(); + }); +}; + +export default class Client { + public constructor(readonly host: string, readonly port: number) { + } + + public getAccount(address: string): Promise { + return request(null, this._constructOptions('GET', `/account/${address}`)) + .then((response: string) => { + let account = JSONBig.parse(response); + if (typeof account.balance === 'object') { + account.balance = account.balance.toFormat(0); + } + return account + }) + .catch(() => error('Could not fetch account.')); + } + + public testConnection(): Promise { + return request(null, this._constructOptions('GET', '/info')) + .then(() => true) + .catch(() => error('Could connect to node.')); + } + + public getAccounts(): Promise { + return request(null, this._constructOptions('GET', '/accounts')) + .then((response: string) => { + let json = JSONBig.parse(response); + if (json.accounts) { + return json.accounts.map((account) => { + if (typeof account.balance === 'object') { + account.balance = account.balance.toFormat(0); + } + return account + }); + } else { + return [] + } + }) + .catch(() => error('Could not fetch accounts.')); + } + + public getInfo(): Promise { + return request(null, this._constructOptions('GET', '/info')) + .then((response: string) => JSONBig.parse(response)) + .catch(() => error('Could not fetch information.')); + } + + public call(tx: string): Promise { + return request(tx, this._constructOptions('POST', '/call')) + .then((response) => response) + .catch(err => error(err)); + } + + public sendTx(tx: string): Promise { + return request(tx, this._constructOptions('POST', '/tx')) + .then((response) => response) + .catch(err => error(err)); + } + + public sendRawTx(tx: string): Promise { + return request(tx, this._constructOptions('POST', '/rawtx')) + .then((response) => response) + } + + public getReceipt(txHash: string): Promise { + return request(null, this._constructOptions('GET', `/tx/${txHash}`)) + .then((response: string) => JSONBig.parse(response)) + .catch(() => error(`Could not fetch receipt for hash: ${txHash}`)); + } + + private _constructOptions(method, path: string) { + return { + host: this.host, + port: this.port, + method: method, + path: path + } + } +} diff --git a/lib/src/evm/SolidityContract.ts b/lib/src/evm/SolidityContract.ts new file mode 100644 index 0000000..a101e22 --- /dev/null +++ b/lib/src/evm/SolidityContract.ts @@ -0,0 +1,212 @@ +/** + * @file SolidityContract.js + * @author Mosaic Networks + * @date 2018 + */ + +import * as Web3 from 'web3' +import * as coder from 'web3/lib/solidity/coder.js' + +import * as errors from "./utils/errors" +import * as checks from './utils/checks'; + +import {ABI, ContractOptions, TXReceipt} from "./utils/Interfaces"; + +import SolidityFunction from "./SolidityFunction"; +import Controller from "../Controller"; +import Transaction from "./Transaction"; + + +export default class SolidityContract { + + public methods: any; + public web3Contract: any; + public receipt: TXReceipt; + + /** + * Javascript Object representation of a Solidity contract. + * + * Can either be used to deploy a contract or interact with a contract already deployed. + * + * @param {Controller} controller - Controller Javascript instance + * @param {ContractOptions} options - The options of the contract. eg. gas price, gas, address + * @constructor + */ + constructor(public options: ContractOptions, readonly controller: Controller) { + const web3 = new Web3(); + + this.options.address = options.address || ''; + this.web3Contract = web3.eth.contract(this.options.jsonInterface).at(this.options.address); + this.receipt = undefined; + this.methods = {}; + + if (this.options.address !== undefined) + this._attachMethodsToContract(); + } + + /** + * Deploy contract to the blockchain. + * + * Deploys contract to the blockchain and sets the newly acquired address of the contract. + * Also assigns the transaction receipt to this instance. + * + * @param {Object} options - The options for the contract. eg. constructor params, gas, gas price, data + * @returns {SolidityContract} Returns deployed contract with receipt and address attributes + */ + deploy(options?: { parameters?: any[], gas?: number, gasPrice?: any, data?: string }) { + if (this.options.address !== '') + throw errors.ContractAddressFieldSetAndDeployed(); + + this.options.jsonInterface.filter((abi: ABI) => { + if (abi.type === "constructor" && options.parameters) { + checks.requireArgsLength(abi.inputs.length, options.parameters.length); + } + }); + + if (options) { + this.options.data = options.data || this.options.data; + this.options.gas = options.gas || this.options.gas; + this.options.gasPrice = options.gasPrice || this.options.gasPrice; + } + + if (this.options.data) { + let encodedData: string; + + if (options.parameters) + encodedData = this.options.data + this._encodeConstructorParams(options.parameters); + + return new Transaction({ + from: this.controller.defaultOptions.from, + data: encodedData + }, false, undefined, this.controller) + .gas(this.options.gas) + .gasPrice(this.options.gasPrice) + .send().then((receipt: TXReceipt) => { + this.receipt = receipt; + return this.setAddressAndPopulate(this.receipt.contractAddress); + }); + } else { + throw errors.InvalidDataFieldInOptions(); + } + } + + /** + * Sets the address of the contract and populates Solidity contract functions. + * + * @param {string} address - The address to assign to the contract + * @returns {SolidityContract} The contract + */ + setAddressAndPopulate(address: string): this { + this.options.address = address; + this._attachMethodsToContract(); + return this + } + + /** + * Sets the address of the contract. + * + * @param {string} address - The address to assign to the contract + * @returns {SolidityContract} The contract + */ + address(address: string): this { + this.options.address = address; + return this + } + + /** + * Sets the default gas for the contract. + * + * Any functions from the this contract will inherit the `gas` value by default. + * You still have the option to override the value once the transaction instance is instantiated. + * + * @param {number} gas - The gas to assign to the contract + * @returns {SolidityContract} The contract + */ + gas(gas: number): this { + this.options.gas = gas; + return this + } + + /** + * Sets the default gas price for the contract. + * + * Any functions from the this contract will inherit the `gasPrice` value by default. + * You still have the option to override the value once the transaction instance is instantiated. + * + * @param {number} gasPrice - The gas price to assign to the contract + * @returns {SolidityContract} The contract + */ + gasPrice(gasPrice: number): this { + this.options.gasPrice = gasPrice; + return this + } + + /** + * Sets the data for deploying the contract. + * + * @param {string} data - The data of the contract + * @returns {SolidityContract} The contract + */ + data(data: string): this { + this.options.data = data; + return this + } + + /** + * Sets the JSON Interface of the contract. + * + * @param {ABI[]} abis - The JSON Interface of contract + * @returns {SolidityContract} The contract + */ + JSONInterface(abis: ABI[]): this { + this.options.jsonInterface = abis; + return this + } + + /** + * Attaches functions to contract. + * + * Parses function data from ABI and creates Javascript instance representation then adds + * these functions to Contract.methods. + * + * @private + */ + private _attachMethodsToContract(): void { + this.options.jsonInterface.filter((json) => { + return json.type === 'function'; + }) + .map((funcJSON: ABI) => { + let solFunction = new SolidityFunction(funcJSON, this.options.address, this.controller); + + if (this.options.gas !== undefined && this.options.gasPrice !== undefined) { + this.methods[funcJSON.name] = solFunction.generateTransaction.bind(solFunction, { + gas: this.options.gas, + gasPrice: this.options.gasPrice, + }); + } else { + this.methods[funcJSON.name] = solFunction.generateTransaction.bind(solFunction, {}); + } + }) + } + + /** + * Encodes constructor parameters. + * + * @param {Array} params - The parameters to encode + * @private + */ + private _encodeConstructorParams(params: any[]): any { + return this.options.jsonInterface.filter((json) => { + return json.type === 'constructor' && json.inputs.length === params.length; + }) + .map((json) => { + return json.inputs.map((input) => { + return input.type; + }); + }) + .map((types) => { + return coder.encodeParams(types, params); + })[0] || ''; + } + +} \ No newline at end of file diff --git a/lib/src/evm/SolidityFunction.ts b/lib/src/evm/SolidityFunction.ts new file mode 100644 index 0000000..225bd4f --- /dev/null +++ b/lib/src/evm/SolidityFunction.ts @@ -0,0 +1,113 @@ +/** + * @file SolidityFunction.js + * @author Mosaic Networks + * @date 2018 + */ + +import * as SolFunction from 'web3/lib/web3/function.js' +import * as coder from 'web3/lib/solidity/coder.js' + +import * as checks from './utils/checks' + +import {ABI, Input, TX} from './utils/Interfaces' + +import Controller from "../Controller"; +import Transaction from "./Transaction"; + + +export default class SolidityFunction { + + readonly name: string; + readonly _inputTypes: string[]; + readonly _outputTypes: string[]; + readonly _solFunction: SolFunction; + readonly _constant: boolean; + readonly _payable: boolean; + + + /** + * Javascript Object representation of a Solidity function. + * + * @param {ABI} abi - JSON describing the function details + * @param {string} contractAddress - The address of parent contract + * @param {Controller} controller - The controller class + * @constructor + */ + constructor(abi: ABI, readonly contractAddress: string, readonly controller: Controller) { + this.name = abi.name; + this._solFunction = new SolFunction('', abi, ''); + this._constant = (abi.stateMutability === "view" || abi.stateMutability === "pure" || abi.constant); + this._payable = (abi.stateMutability === "payable" || abi.payable); + this._inputTypes = abi.inputs.map((i: Input) => { + return i.type; + }); + this._outputTypes = abi.outputs.map((i: Input) => { + return i.type + }); + } + + /** + * Generates Transaction instance to be sent or called. + * + * Creates the scaffolding needed for the transaction to be executed. + * + * @param {Object} options - The options for the transaction of this function + * @param {Array} funcArgs - A list containing all the parameters of the function + */ + generateTransaction(options: { gas?: number, gasPrice?: number }, ...funcArgs: any[]): Transaction { + this._validateArgs(funcArgs); + + let callData = this._solFunction.getData(); + let tx: TX = { + from: this.controller.defaultOptions.from, + to: this.contractAddress, + }; + + if (options && options.gas !== undefined && options.gasPrice !== undefined) { + tx.gas = options.gas; + tx.gasPrice = options.gasPrice; + } + + tx.data = callData; + + if (tx.value <= 0 && this._payable) + throw Error('Function is payable and requires `value` greater than 0.'); + else if (tx.value > 0 && !this._payable) + throw Error('Function is not payable. Required `value` is 0.'); + + let unpackfn: Function = undefined; + + if (this._constant) + unpackfn = this.unpackOutput.bind(this); + + return new Transaction(tx, this._constant, unpackfn, this.controller); + } + + /** + * Decodes output with the corresponding return types. + * + * @param {string} output - The output string to decode + */ + unpackOutput(output: string): any { + output = output.length >= 2 ? output.slice(2) : output; + let result = coder.decodeParams(this._outputTypes, output); + return result.length === 1 ? result[0] : result; + } + + /** + * Validates arguments to the function. + * + * This checks types as well as length of input arguments to required. + * + * @param {Array} args - The list of arguments for the function + * @private + */ + private _validateArgs(args: any[]): void { + checks.requireArgsLength(this._inputTypes.length, args.length); + + args.map((a, i) => { + checks.requireSolidityTypes(this._inputTypes[i], a); + }); + } + +} \ No newline at end of file diff --git a/lib/src/evm/Transaction.ts b/lib/src/evm/Transaction.ts new file mode 100644 index 0000000..4b9ed96 --- /dev/null +++ b/lib/src/evm/Transaction.ts @@ -0,0 +1,188 @@ +/** + * @file Transaction.js + * @author Mosaic Networks + * @date 2018 + */ + +import * as JSONBig from 'json-bigint' + +import {TX, TXReceipt} from "./utils/Interfaces"; + +import Controller from "../Controller"; + + +export default class Transaction { + + public receipt: TXReceipt; + + /** + * Transaction instance to be sent or called. + * + * @param {TX} _tx - The transaction options eg. gas, gas price, value... + * @param {boolean} constant - If the transaction is constant + * @param {Function} unpackfn - If constant unpack function + * @param {Controller} controller - The controller class + */ + constructor(private _tx: TX, readonly constant: boolean, readonly unpackfn: Function, readonly controller: Controller) { + this.receipt = undefined; + + if (!constant) + this.unpackfn = undefined; + } + + /** + * Send transaction. + * + * This function will mutate the state of the EVM. + * + * @param {Object} options - The options to pass to the transaction + */ + send(options?: { to?: string, from?: string, value?: number, gas?: number, gasPrice?: number }): any { + if (!this.constant) { + if (options) { + this._tx.to = options.to || this._tx.to; + this._tx.from = options.from || this._tx.from; + this._tx.gas = options.gas || this._tx.gas; + this._tx.value = options.value || this._tx.value; + + if (options.gasPrice !== undefined && options.gasPrice >= 0) { + this._tx.gasPrice = options.gasPrice; + } + } + + if (this._tx.gas != null && this._tx.gasPrice != null) { + return this.controller.api.sendTx(JSONBig.stringify(this._tx)) + .then((res: string) => { + let response: { txHash: string } = JSONBig.parse(res); + return response.txHash + }) + .then((txHash) => { + return new Promise((resolve) => setTimeout(resolve, 2000)) + .then(() => { + return this.controller.api.getReceipt(txHash) + }) + }) + .then((resp: string) => { + this.receipt = JSONBig.parse(resp); + return this.receipt; + }) + } else { + throw new Error('gas & gas price not set') + } + } else { + throw new Error('Transaction does not mutate state. Use `call()` instead') + } + } + + /** + * Call transaction. + * + * This function will not mutate the state of the EVM. + * + * @param {Object} options - The options to pass to the transaction + */ + call(options?: { to?: string, from?: string, value?: number, gas?: number, gasPrice?: number }) { + if (this.constant) { + if (options) { + this._tx.to = options.to || this._tx.to; + this._tx.from = options.from || this._tx.from; + this._tx.gas = options.gas || this._tx.gas; + this._tx.value = options.value || this._tx.value; + + if (options.gasPrice !== undefined && options.gasPrice >= 0) { + this._tx.gasPrice = options.gasPrice; + } + } + + if (this._tx.gas != null && this._tx.gasPrice != null) { + return this.controller.api.call(JSONBig.stringify(this._tx)) + .then((response) => { + return JSONBig.parse(response); + }) + .then((obj) => { + return this.unpackfn(Buffer.from(obj.data).toString()); + }); + } else { + throw new Error('gas & gas price not set') + } + } else { + throw new Error('Transaction mutates state. Use `send()` instead') + } + } + + /** + * Return transaction as string. + * + * @returns {string} Transaction as string + */ + toString(): string { + return JSONBig.stringify(this._tx); + } + + /** + * Sets the from of the transaction. + * + * @param {string} from - The from address + * @returns {Transaction} The transaction + */ + from(from: string): this { + this._tx.from = from; + return this + } + + /** + * Sets the to of the transaction. + * + * @param {string} to - The to address + * @returns {Transaction} The transaction + */ + to(to: string): this { + this._tx.to = to; + return this + } + + /** + * Sets the value of the transaction. + * + * @param {number} value - The value of tx + * @returns {Transaction} The transaction + */ + value(value: number): this { + this._tx.value = value; + return this + } + + /** + * Sets the gas of the transaction. + * + * @param {number} gas - The gas of tx + * @returns {Transaction} The transaction + */ + gas(gas: number): this { + this._tx.gas = gas; + return this + } + + /** + * Sets the gas price of the transaction. + * + * @param {number} gasPrice - The gas price of tx + * @returns {Transaction} The transaction + */ + gasPrice(gasPrice: number): this { + this._tx.gasPrice = gasPrice; + return this + } + + /** + * Sets the data of the transaction. + * + * @param {string} data - The data of tx + * @returns {Transaction} The transaction + */ + data(data: string): this { + this._tx.data = data; + return this + } + +} \ No newline at end of file diff --git a/lib/src/evm/Wallet.ts b/lib/src/evm/Wallet.ts new file mode 100644 index 0000000..6821dac --- /dev/null +++ b/lib/src/evm/Wallet.ts @@ -0,0 +1,27 @@ +/** + * @file Wallet.js + * @author Mosaic Networks + * @date 2018 + */ + +class Wallet { + + constructor() { + } + + add() { + } + + remove() { + } + + clear() { + } + + encrypt() { + } + + decrypt() { + } + +} \ No newline at end of file diff --git a/lib/src/evm/utils/Interfaces.ts b/lib/src/evm/utils/Interfaces.ts new file mode 100644 index 0000000..5fb7f3b --- /dev/null +++ b/lib/src/evm/utils/Interfaces.ts @@ -0,0 +1,69 @@ +/** + * @file Interface.js + * @author Mosaic Networks + * @date 2018 + */ + +export interface BaseTX { + gas?: number, + gasPrice?: number, +} + + +export interface BaseAccount { + address: string, + nonce: number, + balance: any +} + +export interface TX extends BaseTX { + from: string, + to?: string, + value?: number, + data?: string +} + +export interface ContractOptions extends BaseTX { + from?: string, + address?: string, + data?: string, + jsonInterface: ABI[] +} + +export interface Input { + name: string, + type: string, +} + +export interface ABI { + constant?: any, + inputs: Input[], + name?: any, + outputs?: any[], + payable: any, + stateMutability: any, + type: any +} + +export interface TXReceipt { + root: string, + transactionHash: string, + from: string, + to?: string, + gasUsed: number, + cumulativeGasUsed: number, + contractAddress: string, + logs: [], + logsBloom: string, + failed: boolean +} + +export interface SolidityCompilerOutput { + contracts: {}, + errors: string[], + sourceList: string[], + sources: {} +} + + + diff --git a/lib/src/evm/utils/checks.ts b/lib/src/evm/utils/checks.ts new file mode 100644 index 0000000..deba413 --- /dev/null +++ b/lib/src/evm/utils/checks.ts @@ -0,0 +1,35 @@ +/** + * @file checks.js + * @author Mosaic Networks + * @date 2018 + */ + +import * as errors from "./errors"; + +export const requireArgsLength = (expected: number, received: number): (boolean | Error) => { + if (expected !== received) { + throw errors.InvalidNumberOfSolidityArgs(expected, received); + } else { + return true + } +}; + +export const requireSolidityTypes = (required, received): (boolean | Error) => { + if (typeof received !== parseSolidityType(required)) { + throw errors.InvalidSolidityType(); + } else { + return true + } +}; + +const parseSolidityType = (type: string): (string | undefined) => { + switch (type.toLowerCase()) { + case 'address': + return 'string'; + } + if (type.toLowerCase().includes('int')) { + return 'number' + } + + return undefined; +}; diff --git a/lib/src/evm/utils/errors.ts b/lib/src/evm/utils/errors.ts new file mode 100644 index 0000000..14643f5 --- /dev/null +++ b/lib/src/evm/utils/errors.ts @@ -0,0 +1,21 @@ +/** + * @file errors.js + * @author Mosaic Networks + * @date 2018 + */ + +export function InvalidNumberOfSolidityArgs(expected: number, received: number) { + return new Error(`Expected ${expected} but got ${received} arguments.`); +} + +export function InvalidSolidityType() { + return new TypeError('Invalid argument type') +} + +export function InvalidDataFieldInOptions() { + return new Error('`data` field must be specified before deploying contract.') +} + +export function ContractAddressFieldSetAndDeployed() { + return new Error('Contract\'s address option is already set. Please reset to undefined to deploy.') +} \ No newline at end of file diff --git a/lib/tests/Controller.test.js b/lib/tests/Controller.test.js new file mode 100644 index 0000000..e69de29 diff --git a/lib/tests/Controller.test.ts b/lib/tests/Controller.test.ts new file mode 100644 index 0000000..e69de29 diff --git a/lib/tsconfig.json b/lib/tsconfig.json new file mode 100644 index 0000000..ce456b5 --- /dev/null +++ b/lib/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "outDir": "./dist" + }, + "files": [ + "./node_modules/@types/mocha/index.d.ts", + "../node_modules/@types/node/index.d.ts" + ], + "include": [ + "./src/**/*.ts" + ], + "exclude": [ + "../node_modules" + ] +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..8e6f286 --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "scripts": { + "clean:cli": "rm -rf cli/node_modules && rm -rf cli/package-lock.json", + "clean:lib": "rm -rf lib/node_modules && rm -rf libpackage-lock.json", + "build:lib": "cd lib && npm install && cd ..", + "build:cli": "cd cli && npm install && npm link && cd .." + } +}