├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc ├── README.md ├── bin ├── adamant.js └── init.js ├── config.default.jsonc ├── lib ├── account.js ├── api │ ├── account.js │ ├── delegate.js │ ├── get.js │ ├── index.js │ ├── node.js │ ├── send.js │ └── vote.js ├── delegate.js ├── get.js ├── node.js ├── rpc.js ├── send.js └── vote.js ├── package-lock.json ├── package.json ├── prompt ├── history.js └── index.js └── utils ├── api.js ├── config.js ├── log.js ├── package.js └── validate.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | ecmaVersion: 2020, 4 | }, 5 | env: { 6 | commonjs: true, 7 | es2020: true, 8 | node: true, 9 | jest: true, 10 | }, 11 | extends: ['airbnb-base', 'prettier'], 12 | rules: { 13 | 'no-restricted-syntax': 'off', 14 | 'no-console': 'off', 15 | 'no-underscore-dangle': 'off', 16 | 'default-param-last': 'off', 17 | 'import/extensions': 'always', 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # config environment variable files 2 | config.json 3 | config.jsonc 4 | 5 | # Code editors settings 6 | .vscode 7 | 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | lerna-debug.log* 15 | .pnpm-debug.log* 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | *.lcov 23 | 24 | # Dependency directories 25 | node_modules/ 26 | 27 | # Optional npm cache directory 28 | .npm 29 | 30 | # Optional eslint cache 31 | .eslintcache 32 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | config.default.jsonc 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Adamant-console 2 | 3 | Adamant-console is Command-line utilities to interact with ADAMANT blockchain. It allows you to run commands like send tokens, create new address and get information. 4 | 5 | ## Understanding interaction with ADAMANT blockchain 6 | 7 | ADAMANT has _only secure API_, and you cannot transfer passphrase to node to make any action with wallet. Instead, node _requires signed transaction_ to make any action. 8 | 9 | Adamant-console connects to ADAMANT nodes on your choice (set in configuration file), it can be any node, locally installed on your machine, or from other side of the Earth. As Console doesn’t transfer passphrases to nodes, it's safe to connect to any node. Node you connect should have [API enabled](https://medium.com/adamant-im/how-to-run-your-adamant-node-on-ubuntu-990e391e8fcc#fe7e). 10 | 11 | You can use any programming languages to interact with Adamant-console, like PHP, Python, NodeJS, bash. 12 | 13 | Alternative ways to interact with ADAMANT blockchain: 14 | 15 | - [Direct node's API](https://github.com/Adamant-im/adamant/wiki) 16 | - [JS API library](https://github.com/Adamant-im/adamant-api-jsclient/wiki) 17 | 18 | ## Installing and configuring 19 | 20 | The installation and configuration are described in [Adamant-console Wiki](https://github.com/Adamant-im/adamant-console/wiki/Installation-and-configuration). 21 | 22 | Note, by default, `network` parameter set to `testnet`. If you want to work with mainnet, set the value to `mainnet`. 23 | 24 | ## Using Console 25 | 26 | There are 3 ways of using ADAMANT Console tool: 27 | 28 | - Command-line interface (CLI). List of available for commands see in [Adamant-console Wiki](https://github.com/Adamant-im/adamant-console/wiki/Available-Commands). 29 | - JSON-RPC. To use this interface, [start JSON-RPC daemon](https://github.com/Adamant-im/adamant-console/wiki/JSON-RPC) on Adamant-console. 30 | - Directly through the built-in library. The available methods see in [Adamant-console Wiki](https://github.com/Adamant-im/adamant-console/wiki/Running-Commands-in-Adamant-library) also. 31 | 32 | See [Running Commands in Adamant console](https://github.com/Adamant-im/adamant-console/wiki/Running-Commands-in-Adamant-console). 33 | 34 | ## Integration notes with ADM token for Exchanges 35 | 36 | See [Integration notes with ADM token for Exchanges](https://medium.com/adamant-im/integration-notes-with-adm-token-for-exchanges-d51a80c36aaf). Document describes how to create accounts for deposits, get balances and transactions info, as well as, how to make withdrawals. 37 | -------------------------------------------------------------------------------- /bin/adamant.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { program, CommanderError } from 'commander'; 4 | import chalk from 'chalk'; 5 | import { parse as parseShell } from 'shell-quote'; 6 | 7 | import { satisfies } from 'semver'; 8 | import leven from 'leven'; 9 | 10 | import prompt from '../prompt/index.js'; 11 | 12 | import { log } from '../utils/log.js'; 13 | import config from '../utils/config.js'; 14 | 15 | import { packageInfo } from '../utils/package.js'; 16 | 17 | import installInitCommand from './init.js'; 18 | 19 | import installAccountCommands from '../lib/account.js'; 20 | import installGetCommands from '../lib/get.js'; 21 | import installNodeCommands from '../lib/node.js'; 22 | import installSendCommands from '../lib/send.js'; 23 | import installRpcServerCommands from '../lib/rpc.js'; 24 | import installDelegateCommands from '../lib/delegate.js'; 25 | import installVoteCommands from '../lib/vote.js'; 26 | 27 | const INTERACTIVE_MODE = process.argv.length < 3; 28 | 29 | if (INTERACTIVE_MODE) { 30 | program.exitOverride(); 31 | } 32 | 33 | const requiredVersion = packageInfo.engines.node; 34 | 35 | const checkNodeVersion = (wanted, id) => { 36 | if (!satisfies(process.version, wanted, { includePrerelease: true })) { 37 | console.log( 38 | chalk.red( 39 | `You are using Node ${process.version}, but this version of ${id}` + 40 | ` requires Node ${wanted}.\nPlease upgrade your Node version.`, 41 | ), 42 | ); 43 | 44 | process.exit(1); 45 | } 46 | }; 47 | 48 | checkNodeVersion(requiredVersion, 'adamant-console'); 49 | 50 | const suggestCommands = (unknownCommand) => { 51 | const availableCommands = program.commands.map((cmd) => cmd._name); 52 | 53 | let suggestion; 54 | 55 | availableCommands.forEach((cmd) => { 56 | const isBestMatch = 57 | leven(cmd, unknownCommand) < leven(suggestion || '', unknownCommand); 58 | 59 | if (leven(cmd, unknownCommand) < 3 && isBestMatch) { 60 | suggestion = cmd; 61 | } 62 | }); 63 | 64 | if (suggestion) { 65 | console.log(); 66 | console.log(` ${chalk.red(`Did you mean ${chalk.yellow(suggestion)}?`)}`); 67 | } 68 | }; 69 | 70 | program 71 | .name('adm') 72 | .version(`adm ${packageInfo.version}`) 73 | .usage(' [options]') 74 | .option('-p, --passphrase ', 'account passphrase'); 75 | 76 | installAccountCommands(program); 77 | installGetCommands(program); 78 | installNodeCommands(program); 79 | installSendCommands(program); 80 | installRpcServerCommands(program); 81 | installDelegateCommands(program); 82 | installVoteCommands(program); 83 | installInitCommand(program); 84 | 85 | const client = program.command('client'); 86 | 87 | client.command('version').action(() => { 88 | log({ 89 | success: true, 90 | version: packageInfo.version, 91 | config: config.configPath, 92 | network: config.network, 93 | account: config.accountAddress, 94 | }); 95 | }); 96 | 97 | program.on('option:passphrase', () => { 98 | config.passphrase = program.opts().passphrase; 99 | }); 100 | 101 | // output help information on unknown commands 102 | program.on('command:*', ([cmd]) => { 103 | program.outputHelp(); 104 | 105 | console.log(` ${chalk.red(`Unknown command ${chalk.yellow(cmd)}.`)}`); 106 | 107 | suggestCommands(cmd); 108 | 109 | process.exitCode = 1; 110 | }); 111 | 112 | // add some useful info on help 113 | program.on('--help', () => { 114 | console.log(); 115 | console.log( 116 | ` Run ${chalk.cyan('adm --help')} for detailed usage of given command.`, 117 | ); 118 | console.log(); 119 | }); 120 | 121 | if (INTERACTIVE_MODE) { 122 | prompt(async (command) => { 123 | try { 124 | await program.parseAsync(parseShell(command), { from: 'user' }); 125 | } catch (err) { 126 | if (!(err instanceof CommanderError)) { 127 | console.log(err); 128 | } 129 | } 130 | }); 131 | } else { 132 | program.parse(process.argv); 133 | } 134 | -------------------------------------------------------------------------------- /bin/init.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | 5 | import { configFileName, configDirPath } from '../utils/config.js'; 6 | 7 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 8 | 9 | export default (program) => { 10 | program 11 | .command('init') 12 | .description( 13 | `Copies default config file into the given path directory or inside ${configDirPath}`, 14 | ) 15 | .argument('[path]', 'directory path to copy config into') 16 | .action(async (targetDirectory = configDirPath) => { 17 | if (!fs.existsSync(targetDirectory)) { 18 | fs.mkdirSync(targetDirectory, { recursive: true }); 19 | } 20 | 21 | const defaultConfigPath = path.join(__dirname, '../config.default.jsonc'); 22 | const targetFilePath = path.resolve(targetDirectory, configFileName); 23 | 24 | if (fs.existsSync(targetFilePath)) { 25 | console.error( 26 | `Error: The file ${configFileName} already exists in '${targetDirectory}'. Please remove or rename it.`, 27 | ); 28 | return; 29 | } 30 | 31 | fs.copyFile(defaultConfigPath, targetFilePath, (error) => { 32 | if (error) { 33 | console.error('Error copying the config file:', error); 34 | } else { 35 | console.log( 36 | `Config was successfully initialized in ${targetFilePath}`, 37 | ); 38 | console.log('Edit it using the following command:'); 39 | console.log(` nano '${targetFilePath}'`); 40 | } 41 | }); 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /config.default.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | * The Console will use this ADM passphrase by default. 4 | * See 'Installing and configuring' in Readme for details. 5 | */ 6 | "passphrase": "distance expect praise frequent..", 7 | 8 | /* Choose 'mainnet' or 'testnet' */ 9 | "network": "testnet", 10 | 11 | /* Port used to run RPC server */ 12 | "rpc": { 13 | "port": 5080 14 | }, 15 | 16 | /* Additionally, you can specify what ADM nodes to use */ 17 | "networks": { 18 | "testnet": { 19 | "nodes": [ 20 | { 21 | "ip": "127.0.0.1", 22 | "protocol": "http", 23 | "port": 36667 24 | } 25 | ] 26 | }, 27 | "mainnet": { 28 | "nodes": [ 29 | { 30 | "ip": "clown.adamant.im", 31 | "protocol": "https", 32 | "port": "" 33 | }, 34 | { 35 | "ip": "lake.adamant.im", 36 | "protocol": "https", 37 | "port": "" 38 | }, 39 | { 40 | "ip": "endless.adamant.im", 41 | "protocol": "https", 42 | "port": "" 43 | } 44 | ] 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/account.js: -------------------------------------------------------------------------------- 1 | import * as api from './api/index.js'; 2 | import * as log from '../utils/log.js'; 3 | 4 | export default (program) => { 5 | const account = program.command('account'); 6 | 7 | account 8 | .command('new') 9 | .description( 10 | 'creates new ADAMANT account and provide account data in JSON format', 11 | ) 12 | .action(log.call(api.createAccount)); 13 | }; 14 | -------------------------------------------------------------------------------- /lib/api/account.js: -------------------------------------------------------------------------------- 1 | import { 2 | createNewPassphrase, 3 | createKeypairFromPassphrase, 4 | createAddressFromPublicKey, 5 | } from 'adamant-api'; 6 | 7 | export const createAccount = () => { 8 | const passphrase = createNewPassphrase(); 9 | const keypair = createKeypairFromPassphrase(passphrase); 10 | 11 | const answer = { 12 | success: true, 13 | account: { 14 | passphrase, 15 | address: createAddressFromPublicKey(keypair.publicKey), 16 | publicKey: keypair.publicKey.toString('hex'), 17 | privateKey: keypair.privateKey.toString('hex'), 18 | }, 19 | }; 20 | 21 | return answer; 22 | }; 23 | -------------------------------------------------------------------------------- /lib/api/delegate.js: -------------------------------------------------------------------------------- 1 | import api from '../../utils/api.js'; 2 | import config from '../../utils/config.js'; 3 | import { requiredParam } from '../../utils/validate.js'; 4 | 5 | export async function createDelegate( 6 | username = requiredParam('username'), 7 | passphrase, 8 | ) { 9 | const res = await api.newDelegate(passphrase || config.passphrase, username); 10 | 11 | return res.data || res; 12 | } 13 | -------------------------------------------------------------------------------- /lib/api/get.js: -------------------------------------------------------------------------------- 1 | import { 2 | createKeypairFromPassphrase, 3 | createAddressFromPublicKey, 4 | decodeMessage, 5 | TransactionType, 6 | } from 'adamant-api'; 7 | 8 | import api from '../../utils/api.js'; 9 | import config from '../../utils/config.js'; 10 | 11 | import { requiredParam } from '../../utils/validate.js'; 12 | 13 | export async function getAddress(address = requiredParam('address')) { 14 | const res = await api.getAccountInfo({ address }); 15 | 16 | return res.data || res; 17 | } 18 | 19 | export async function getBlock(id = requiredParam('id')) { 20 | const res = await api.getBlock(id); 21 | 22 | return res.data || res; 23 | } 24 | 25 | export async function getBlocks(...queries) { 26 | const res = await api.getBlocks(queryStringToObject(queries)); 27 | 28 | return res.data || res; 29 | } 30 | 31 | export async function getDelegate(username = requiredParam('username')) { 32 | const res = await api.getDelegate({ username }); 33 | 34 | return res.data || res; 35 | } 36 | 37 | export async function getMessage(id = requiredParam('id'), customPassphrase) { 38 | const res = await api.getTransaction(id, { returnAsset: 1 }); 39 | 40 | const passphrase = customPassphrase || config.passphrase; 41 | 42 | if (!passphrase) { 43 | return res; 44 | } 45 | 46 | const { transaction, error } = res; 47 | 48 | if (error) { 49 | return { 50 | success: false, 51 | error, 52 | }; 53 | } 54 | 55 | if (transaction?.type !== TransactionType.CHAT_MESSAGE) { 56 | return { 57 | success: false, 58 | error: 'Not a message transaction', 59 | }; 60 | } 61 | 62 | if (transaction.asset?.chat.own_message) { 63 | const keypair = createKeypairFromPassphrase(passphrase); 64 | const readerAddress = createAddressFromPublicKey(keypair.publicKey); 65 | 66 | if ( 67 | ![transaction.senderId, transaction.recipientId].includes(readerAddress) 68 | ) { 69 | return res; 70 | } 71 | 72 | const recipientName = 73 | transaction.senderId === readerAddress 74 | ? transaction.recipientId 75 | : transaction.senderId; 76 | 77 | const publicKey = await api.getPublicKey(recipientName); 78 | 79 | if (publicKey) { 80 | const decoded = decodeMessage( 81 | transaction.asset.chat.message, 82 | publicKey, 83 | keypair, 84 | transaction.asset.chat.own_message, 85 | ); 86 | 87 | transaction.decoded = decoded; 88 | } 89 | } 90 | 91 | return res; 92 | } 93 | 94 | export async function getTransaction(id = requiredParam('id')) { 95 | const res = await api.getTransaction(id); 96 | 97 | return res.data || res; 98 | } 99 | 100 | export async function getTransactions(...queries) { 101 | const res = await api.getTransactions(queryStringToObject(queries)); 102 | 103 | return res.data || res; 104 | } 105 | 106 | export async function getTransactionsInBlockById( 107 | blockId = requiredParam('blockId'), 108 | ) { 109 | return getTransactions(`blockId=${blockId}`, 'orderBy=timestamp:desc'); 110 | } 111 | 112 | export async function getTransactionsInBlockByHeight( 113 | height = requiredParam('height'), 114 | ) { 115 | return getTransactions( 116 | `fromHeight=${height}`, 117 | `and:toHeight=${height}`, 118 | 'orderBy=timestamp:desc', 119 | ); 120 | } 121 | 122 | export async function getTransactionsReceivedByAddress( 123 | address = requiredParam('address'), 124 | ) { 125 | return getTransactions( 126 | `recipientId=${address}`, 127 | 'and:minAmount=1', 128 | 'orderBy=timestamp:desc', 129 | ); 130 | } 131 | 132 | function queryStringToObject(queries) { 133 | const params = new URLSearchParams(queries.join('&').replace(/,/g, '&')); 134 | const result = {}; 135 | 136 | for (const [key, value] of params.entries()) { 137 | result[key] = value; 138 | } 139 | 140 | return result; 141 | } 142 | -------------------------------------------------------------------------------- /lib/api/index.js: -------------------------------------------------------------------------------- 1 | export * from './account.js'; 2 | export * from './get.js'; 3 | export * from './node.js'; 4 | export * from './send.js'; 5 | export * from './delegate.js'; 6 | export * from './vote.js'; 7 | -------------------------------------------------------------------------------- /lib/api/node.js: -------------------------------------------------------------------------------- 1 | import api from '../../utils/api.js'; 2 | 3 | export async function getNodeHeight() { 4 | const res = await api.getHeight(); 5 | 6 | return res.data || res; 7 | } 8 | 9 | export async function getNodeVersion() { 10 | const res = await api.getNodeVersion(); 11 | 12 | return res.data || res; 13 | } 14 | -------------------------------------------------------------------------------- /lib/api/send.js: -------------------------------------------------------------------------------- 1 | import { MessageType } from 'adamant-api'; 2 | 3 | import api from '../../utils/api.js'; 4 | import config from '../../utils/config.js'; 5 | import { requiredParam } from '../../utils/validate.js'; 6 | 7 | const prepareJSON = (json) => json.replace(/'/g, '"').replace(/\\line/g, '\n'); 8 | 9 | export async function sendTokens( 10 | address = requiredParam('address'), 11 | amountString = requiredParam('amountString'), 12 | passphrase, 13 | ) { 14 | const isAmountInADM = amountString.includes('ADM'); 15 | const amount = parseFloat(amountString, 10); 16 | 17 | const res = await api.sendTokens( 18 | passphrase || config.passphrase, 19 | address, 20 | amount, 21 | isAmountInADM, 22 | ); 23 | 24 | return res.data || res; 25 | } 26 | 27 | export async function sendMessage( 28 | address = requiredParam('address'), 29 | message = requiredParam('message'), 30 | amountString = '', 31 | passphrase, 32 | ) { 33 | const messageType = MessageType.Chat; 34 | const isAmountInADM = amountString.includes('ADM'); 35 | const amount = parseFloat(amountString, 10); 36 | 37 | const messageStr = 38 | typeof message === 'object' ? JSON.stringify(message) : message; 39 | 40 | const res = await api.sendMessage( 41 | passphrase || config.passphrase, 42 | address, 43 | messageStr, 44 | messageType, 45 | amount, 46 | isAmountInADM, 47 | ); 48 | 49 | return res.data || res; 50 | } 51 | 52 | export async function sendRich( 53 | address = requiredParam('address'), 54 | json = requiredParam('json'), 55 | passphrase, 56 | ) { 57 | const messageType = MessageType.Rich; 58 | const message = 59 | typeof json === 'object' ? JSON.stringify(json) : prepareJSON(json); 60 | 61 | const res = await api.sendMessage( 62 | passphrase || config.passphrase, 63 | address, 64 | message, 65 | messageType, 66 | ); 67 | 68 | return res.data || res; 69 | } 70 | 71 | export async function sendSignal( 72 | address = requiredParam('address'), 73 | json = requiredParam('json'), 74 | passphrase, 75 | ) { 76 | const messageType = MessageType.Signal; 77 | const message = 78 | typeof json === 'object' ? JSON.stringify(json) : prepareJSON(json); 79 | 80 | const res = await api.sendMessage( 81 | passphrase || config.passphrase, 82 | address, 83 | message, 84 | messageType, 85 | ); 86 | 87 | return res.data || res; 88 | } 89 | -------------------------------------------------------------------------------- /lib/api/vote.js: -------------------------------------------------------------------------------- 1 | import api from '../../utils/api.js'; 2 | import config from '../../utils/config.js'; 3 | import { requiredParam } from '../../utils/validate.js'; 4 | 5 | export async function voteFor( 6 | delegateArr = requiredParam('delegates'), 7 | passphrase, 8 | ) { 9 | const delegates = delegateArr; 10 | 11 | for (const [index, delegate] of delegates.entries()) { 12 | const voteDirection = delegate.charAt(0); 13 | 14 | if (!['+', '-'].includes(voteDirection)) { 15 | delegates[index] = `+${delegate}`; 16 | } 17 | } 18 | 19 | const res = await api.voteForDelegate( 20 | passphrase || config.passphrase, 21 | delegates, 22 | ); 23 | 24 | return res.data || res; 25 | } 26 | -------------------------------------------------------------------------------- /lib/delegate.js: -------------------------------------------------------------------------------- 1 | import * as api from './api/index.js'; 2 | import * as log from '../utils/log.js'; 3 | 4 | export default (program) => { 5 | const delegate = program.command('delegate'); 6 | 7 | delegate 8 | .command('new ') 9 | .description( 10 | 'registers user account as delegate and provide delegate data in JSON format.', 11 | ) 12 | .action((username) => log.call(api.createDelegate)(username)); 13 | }; 14 | -------------------------------------------------------------------------------- /lib/get.js: -------------------------------------------------------------------------------- 1 | import * as api from './api/index.js'; 2 | import * as log from '../utils/log.js'; 3 | 4 | export default (program) => { 5 | const get = program.command('get'); 6 | 7 | get 8 | .command('address
') 9 | .description( 10 | 'returns information about ADAMANT network address (account), information returned consists of balance and publicKey.', 11 | ) 12 | .action((address) => log.call(api.getAddress)(address)); 13 | 14 | get 15 | .command('block ') 16 | .description( 17 | 'returns block information, which contains information about forger (generatorId), timestamp, signatures, and other fields.', 18 | ) 19 | .action((id) => log.call(api.getBlock)(id)); 20 | 21 | get 22 | .command('blocks ') 23 | .description( 24 | 'returns array of blocks in ADAMANT chain from newest to oldest.', 25 | ) 26 | .action((queries) => log.call(api.getBlocks)(queries)); 27 | 28 | get 29 | .command('delegate ') 30 | .description("returns information about delegate and it's productivity") 31 | .action((username) => log.call(api.getDelegate)(username)); 32 | 33 | get 34 | .command('message ') 35 | .description( 36 | 'returns information about message and the message itself decoded. Works the same way as get transaction, but returns asset decoded.', 37 | ) 38 | .action((id) => log.call(api.getMessage)(id)); 39 | 40 | get 41 | .command('transaction ') 42 | .description('returns information about specific transaction') 43 | .action((id) => log.call(api.getTransaction)(id)); 44 | 45 | get 46 | .command('transactions ') 47 | .description('performs complex queries to transactions store') 48 | .action((queries) => log.call(api.getTransactions)(queries)); 49 | }; 50 | -------------------------------------------------------------------------------- /lib/node.js: -------------------------------------------------------------------------------- 1 | import * as api from './api/index.js'; 2 | import * as log from '../utils/log.js'; 3 | 4 | export default (program) => { 5 | const node = program.command('node'); 6 | 7 | node 8 | .command('height') 9 | .description("returns current node's blockchain height.") 10 | .action(log.call(api.getNodeHeight)); 11 | 12 | node 13 | .command('version') 14 | .description( 15 | "returns node's software information: version, build and commit.", 16 | ) 17 | .action(log.call(api.getNodeVersion)); 18 | }; 19 | -------------------------------------------------------------------------------- /lib/rpc.js: -------------------------------------------------------------------------------- 1 | import rpc from 'jayson/promise/index.js'; 2 | 3 | import * as api from './api/index.js'; 4 | import * as log from '../utils/log.js'; 5 | import config from '../utils/config.js'; 6 | 7 | import { packageInfo } from '../utils/package.js'; 8 | 9 | /** 10 | * Safe callback and error handling 11 | * @param {Function} func Callback function 12 | * @returns { (...args) => Promise } 13 | */ 14 | const call = 15 | (func) => 16 | async (...args) => { 17 | try { 18 | return await func(...args); 19 | } catch (error) { 20 | return { 21 | success: false, 22 | error: error.toString(), 23 | }; 24 | } 25 | }; 26 | 27 | /** 28 | * Returns a function that calls `func` with argument list argArray 29 | * 30 | * Example: callWithArgs(func)([1, 2]) => func(1, 2) 31 | * @param {Function} func Callback function 32 | * @returns { (argArray: any[]) => Promise } 33 | */ 34 | const callWithArgs = (func, callback) => 35 | async function f(argArray) { 36 | if (!Array.isArray(argArray)) { 37 | return { 38 | success: false, 39 | error: '"params" parameter should be an array', 40 | }; 41 | } 42 | 43 | const res = await call(func)(...argArray); 44 | 45 | if (res.error) { 46 | throw this.error(501, res.error); 47 | } 48 | 49 | if (typeof callback === 'function') { 50 | return callback(res); 51 | } 52 | 53 | return res; 54 | }; 55 | 56 | /** 57 | * Returns a function that calls `func` with a list of arguments in the form of 58 | * argObject values specified in the template 59 | * 60 | * Example: callWithTemplate(func, ['arg1', 'arg2'])({ arg1: 1, arg2: 2 }) => func(1, 2) 61 | * @param {Function} func Callback function 62 | * @returns { (argArray: any[]) => Promise } 63 | */ 64 | const callWithTemplate = (func, template = [], callback) => 65 | async function f(argObject) { 66 | const argArray = []; 67 | 68 | if (typeof argObject === 'object') { 69 | for (const key of template) { 70 | argArray.push(argObject[key]); 71 | } 72 | } 73 | 74 | const res = await call(func)(...argArray); 75 | 76 | if (res.error) { 77 | throw this.error(501, res.error); 78 | } 79 | 80 | if (typeof callback === 'function') { 81 | return callback(res); 82 | } 83 | 84 | return res; 85 | }; 86 | 87 | export default (program) => { 88 | const rpcCommand = program.command('rpc'); 89 | 90 | rpcCommand 91 | .command('server') 92 | .description('runs JSON-RPC server.') 93 | .action(() => { 94 | const server = new rpc.Server({ 95 | async clientVersion() { 96 | return { 97 | success: true, 98 | version: packageInfo.version, 99 | }; 100 | }, 101 | 102 | accountNew: callWithArgs(api.createAccount, (data) => data.account), 103 | 104 | delegateNew: callWithArgs(api.createDelegate, (data) => data.account), 105 | 106 | getAddress: callWithArgs(api.getAddress, (data) => data.account), 107 | 108 | getBlock: callWithArgs(api.getBlock, (data) => data.block), 109 | 110 | getBlocks: callWithArgs(api.getBlocks, (data) => data.blocks), 111 | 112 | getDelegate: callWithArgs(api.getDelegate, (data) => data.delegate), 113 | 114 | getMessage: callWithArgs(api.getMessage, (data) => data.transaction), 115 | 116 | getTransaction: callWithArgs( 117 | api.getTransaction, 118 | (data) => data.transaction, 119 | ), 120 | 121 | getTransactionsInBlockById: callWithArgs( 122 | api.getTransactionsInBlockById, 123 | (data) => data.transactions, 124 | ), 125 | 126 | getTransactionsInBlockByHeight: callWithArgs( 127 | api.getTransactionsInBlockByHeight, 128 | (data) => data.transactions, 129 | ), 130 | 131 | getTransactionsReceivedByAddress: callWithArgs( 132 | api.getTransactionsReceivedByAddress, 133 | (data) => data.transactions, 134 | ), 135 | 136 | getTransactions: callWithArgs( 137 | api.getTransactions, 138 | (data) => data.transactions, 139 | ), 140 | 141 | nodeHeight: callWithArgs(api.getNodeHeight, (data) => data.height), 142 | 143 | nodeVersion: callWithArgs( 144 | api.getNodeVersion, 145 | ({ commit, version }) => ({ commit, version }), 146 | ), 147 | 148 | sendTokens: callWithTemplate( 149 | api.sendTokens, 150 | ['address', 'amount', 'passphrase'], 151 | (data) => data.transactionId, 152 | ), 153 | 154 | sendMessage: callWithTemplate( 155 | api.sendMessage, 156 | ['address', 'message', 'amount', 'passphrase'], 157 | (data) => data.transactionId, 158 | ), 159 | 160 | sendRich: callWithTemplate( 161 | api.sendRich, 162 | ['address', 'data', 'passphrase'], 163 | (data) => data.transactionId, 164 | ), 165 | 166 | sendSignal: callWithTemplate( 167 | api.sendSignal, 168 | ['address', 'data', 'passphrase'], 169 | (data) => data.transactionId, 170 | ), 171 | 172 | voteFor: callWithTemplate( 173 | api.voteFor, 174 | ['votes', 'passphrase'], 175 | (data) => data.transaction, 176 | ), 177 | }); 178 | 179 | const { port } = config.rpc; 180 | 181 | server.http().listen(port); 182 | 183 | log.log({ 184 | success: true, 185 | data: `JSON-RPC server listening on port ${port}`, 186 | }); 187 | }); 188 | }; 189 | -------------------------------------------------------------------------------- /lib/send.js: -------------------------------------------------------------------------------- 1 | import * as api from './api/index.js'; 2 | import * as log from '../utils/log.js'; 3 | 4 | export default (program) => { 5 | const send = program.command('send'); 6 | 7 | send 8 | .command('tokens
') 9 | .description( 10 | 'sends tokens from account. Note: to send tokens with comment, use "send message" instead.', 11 | ) 12 | .action((address, amount) => log.call(api.sendTokens)(address, amount)); 13 | 14 | send 15 | .command('message
[amount]') 16 | .description('sends message from account.') 17 | .action((address, message, amount) => 18 | log.call(api.sendMessage)(address, message, amount), 19 | ); 20 | 21 | send 22 | .command('rich
') 23 | .description('sends rich message.') 24 | .action((address, json) => log.call(api.sendRich)(address, json)); 25 | 26 | send 27 | .command('signal
') 28 | .description('sends signal message.') 29 | .action((address, json) => log.call(api.sendSignal)(address, json)); 30 | }; 31 | -------------------------------------------------------------------------------- /lib/vote.js: -------------------------------------------------------------------------------- 1 | import * as api from './api/index.js'; 2 | import * as log from '../utils/log.js'; 3 | 4 | export default (program) => { 5 | const vote = program.command('vote'); 6 | 7 | vote 8 | .command('for ') 9 | .description('votes for delegates in ADAMANT blockchain.') 10 | .action((delegates) => log.call(api.voteFor)(delegates)); 11 | }; 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adamant-console", 3 | "version": "3.0.0", 4 | "description": "Console API and JSON-RPC for interacting with ADAMANT Blockchain", 5 | "main": "lib/api/index.js", 6 | "type": "module", 7 | "bin": { 8 | "adm": "./bin/adamant.js" 9 | }, 10 | "engines": { 11 | "node": ">=14" 12 | }, 13 | "author": "ADAMANT Foundation (https://adamant.im)", 14 | "license": "GPL-3.0", 15 | "scripts": { 16 | "format": "prettier . --write", 17 | "test": "echo \"Error: no test specified\" && exit 1", 18 | "lint": "npx eslint -f visualstudio .", 19 | "lint:fix": "npx eslint -f visualstudio --fix" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/Adamant-im/adamant-console.git" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/Adamant-im/adamant-console/issues" 27 | }, 28 | "homepage": "https://github.com/Adamant-im/adamant-console#readme", 29 | "devDependencies": { 30 | "eslint": "^8.57.0", 31 | "eslint-config-airbnb-base": "^15.0.0", 32 | "eslint-config-prettier": "^9.1.0", 33 | "eslint-plugin-import": "^2.29.1", 34 | "prettier": "3.3.2" 35 | }, 36 | "dependencies": { 37 | "@stablelib/utf8": "^1.0.2", 38 | "adamant-api": "^2.4.0", 39 | "bitcore-mnemonic": "^10.0.36", 40 | "chalk": "^5.3.0", 41 | "commander": "^12.1.0", 42 | "ed2curve": "^0.3.0", 43 | "jayson": "^4.1.0", 44 | "joi": "^17.13.1", 45 | "jsonminify": "^0.4.2", 46 | "leven": "^4.0.0", 47 | "semver": "^7.6.2", 48 | "shell-quote": "^1.8.1", 49 | "sodium-browserify-tweetnacl": "^0.2.6", 50 | "tweetnacl": "^1.0.3" 51 | }, 52 | "keywords": [ 53 | "adm", 54 | "adamant", 55 | "blockchain", 56 | "messenger", 57 | "wallet", 58 | "secure", 59 | "encryption", 60 | "crypto", 61 | "cryptocurrency", 62 | "CLI", 63 | "console" 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /prompt/history.js: -------------------------------------------------------------------------------- 1 | export default class History { 2 | constructor() { 3 | this.history = []; 4 | this.current = 0; 5 | } 6 | 7 | add(...args) { 8 | this.current += 1; 9 | 10 | const len = this.history.push(...args); 11 | 12 | if (len > 500) { 13 | this.history.shift(); 14 | 15 | return len - 1; 16 | } 17 | 18 | return len; 19 | } 20 | 21 | next(str) { 22 | if (this.history.length <= this.current) { 23 | return str; 24 | } 25 | 26 | this.current += 1; 27 | 28 | return this.history[this.current]; 29 | } 30 | 31 | back(str) { 32 | if (this.current < 1) { 33 | return str; 34 | } 35 | 36 | this.current -= 1; 37 | 38 | return this.history[this.current]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /prompt/index.js: -------------------------------------------------------------------------------- 1 | import readline from 'readline'; 2 | import History from './history.js'; 3 | import { packageInfo } from '../utils/package.js'; 4 | 5 | export default (callback) => { 6 | const rl = readline.createInterface({ 7 | input: process.stdin, 8 | output: process.stdout, 9 | prompt: '> ', 10 | }); 11 | 12 | const history = new History(); 13 | 14 | console.log(`Welcome to ADM CLI v${packageInfo.version}.`); 15 | console.log('Type "help" for more information.'); 16 | 17 | process.stdin.on('keypress', (eventName, key) => { 18 | if (key.name === 'up') { 19 | const line = history.back(rl.line); 20 | 21 | if (line) { 22 | rl.write(null, { ctrl: true, name: 'u' }); 23 | 24 | rl.write(line); 25 | } 26 | } 27 | 28 | if (key.name === 'down') { 29 | rl.write(null, { ctrl: true, name: 'u' }); 30 | 31 | rl.write(history.next(rl.line)); 32 | } 33 | }); 34 | 35 | let isTryingToExit = false; 36 | 37 | rl.on('SIGINT', () => { 38 | if (rl.line !== '') { 39 | console.log(); 40 | rl.write(null, { ctrl: true, name: 'u' }); 41 | rl.prompt(); 42 | } else if (isTryingToExit) { 43 | process.exit(); 44 | } else { 45 | isTryingToExit = true; 46 | 47 | console.log('\n(To exit, press Ctrl+C again)'); 48 | rl.prompt(true); 49 | } 50 | }); 51 | 52 | rl.on('line', async (line) => { 53 | isTryingToExit = false; 54 | 55 | history.add(line); 56 | 57 | if (line === 'clear') { 58 | console.clear(); 59 | } else { 60 | rl.pause(); 61 | 62 | try { 63 | await callback(line); 64 | } catch (err) { 65 | console.error(err); 66 | console.log(); 67 | } 68 | } 69 | 70 | rl.prompt(true); 71 | }); 72 | 73 | rl.prompt(true); 74 | }; 75 | -------------------------------------------------------------------------------- /utils/api.js: -------------------------------------------------------------------------------- 1 | import { AdamantApi } from 'adamant-api'; 2 | import config from './config.js'; 3 | 4 | const network = config.networks[config.network]; 5 | 6 | const nodes = network.nodes.map( 7 | ({ ip, protocol, port }) => `${protocol}://${ip}${port ? `:${port}` : ''}`, 8 | ); 9 | 10 | // Check health in interactive mode 11 | const checkHealthAtStartup = process.argv.length < 3; 12 | 13 | const api = new AdamantApi({ 14 | nodes, 15 | checkHealthAtStartup, 16 | logLevel: -1, 17 | }); 18 | 19 | export default api; 20 | -------------------------------------------------------------------------------- /utils/config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-dynamic-require */ 2 | 3 | import os from 'os'; 4 | import fs from 'fs'; 5 | import path from 'path'; 6 | import { fileURLToPath } from 'url'; 7 | 8 | import Joi from 'joi'; 9 | import jsonminify from 'jsonminify'; 10 | import chalk from 'chalk'; 11 | 12 | import { 13 | createKeypairFromPassphrase, 14 | createAddressFromPublicKey, 15 | } from 'adamant-api'; 16 | 17 | import * as log from './log.js'; 18 | 19 | const homeDir = os.homedir(); 20 | 21 | const configPathName = '.adm'; 22 | 23 | export const configFileName = process.env.ADM_CONFIG_FILENAME || 'config.jsonc'; 24 | export const configDirPath = 25 | process.env.ADM_CONFIG_PATH || `${homeDir}/${configPathName}`; 26 | 27 | const configFilePath = path.normalize(`${configDirPath}/${configFileName}`); 28 | const localConfigFilePath = path.resolve(configFileName); 29 | 30 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 31 | const defaultConfigFilePath = path.join(__dirname, '../config.default.jsonc'); 32 | 33 | function loadConfig(configPath) { 34 | let parsedConfig = {}; 35 | 36 | try { 37 | const data = fs.readFileSync(configPath, 'utf8'); 38 | 39 | parsedConfig = JSON.parse(jsonminify(data)); 40 | } catch (err) { 41 | console.log( 42 | chalk.red( 43 | 'Failed to parse the configuration from:\n' + 44 | `└── ${chalk.yellow(configPath)}.`, 45 | ), 46 | ); 47 | console.log(); 48 | console.log(err); 49 | process.exit(1); 50 | } 51 | 52 | return parsedConfig; 53 | } 54 | 55 | let config = loadConfig(defaultConfigFilePath); 56 | 57 | const configPaths = [configFilePath, localConfigFilePath]; 58 | 59 | for (const configPath of configPaths) { 60 | let existingConfigPath; 61 | 62 | const configWithComments = `${configPath}c`; 63 | 64 | if (fs.existsSync(configPath)) { 65 | existingConfigPath = configPath; 66 | } else if ( 67 | !process.env.ADM_CONFIG_FILENAME && 68 | fs.existsSync(configWithComments) 69 | ) { 70 | existingConfigPath = configWithComments; 71 | } 72 | 73 | if (existingConfigPath) { 74 | const loadedConfig = loadConfig(existingConfigPath); 75 | 76 | const keypair = createKeypairFromPassphrase(loadedConfig.passphrase); 77 | const address = createAddressFromPublicKey(keypair.publicKey); 78 | 79 | config = { 80 | ...config, 81 | ...loadedConfig, 82 | configPath: existingConfigPath, 83 | accountAddress: address, 84 | }; 85 | 86 | break; 87 | } 88 | } 89 | 90 | const netSchema = Joi.object({ 91 | nodes: Joi.array().items( 92 | Joi.object({ 93 | ip: Joi.string(), 94 | protocol: Joi.string(), 95 | port: Joi.number().allow(''), 96 | }), 97 | ), 98 | }); 99 | 100 | const schema = Joi.object({ 101 | network: Joi.string(), 102 | rpc: Joi.object({ 103 | port: Joi.number(), 104 | }), 105 | networks: Joi.object({ 106 | [config.network]: netSchema.required(), 107 | }), 108 | // passphrase can be set later 109 | passphrase: Joi.string().allow(''), 110 | }); 111 | 112 | const { error, value } = schema.validate(config, { 113 | allowUnknown: true, 114 | }); 115 | 116 | if (error) { 117 | log.error( 118 | error.toString().replace('ValidationError', 'Config validation error'), 119 | ); 120 | } 121 | 122 | export default value; 123 | -------------------------------------------------------------------------------- /utils/log.js: -------------------------------------------------------------------------------- 1 | const stringify = (obj = {}) => JSON.stringify(obj, null, 2); 2 | 3 | export const log = (...args) => { 4 | const res = Object.assign({}, ...args); 5 | 6 | console.log(stringify(res)); 7 | }; 8 | 9 | export const warn = (...args) => { 10 | const output = args.join(' '); 11 | 12 | log({ 13 | success: false, 14 | error: output, 15 | }); 16 | }; 17 | 18 | export const error = (...args) => warn(...args); 19 | 20 | export const call = (func, ...callArgs) => { 21 | return async (...args) => { 22 | try { 23 | const output = await func(...callArgs, ...args); 24 | 25 | log(output); 26 | } catch (error) { 27 | warn(error); 28 | } 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /utils/package.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | 5 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 6 | 7 | export const packageInfo = JSON.parse( 8 | fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8'), 9 | ); 10 | -------------------------------------------------------------------------------- /utils/validate.js: -------------------------------------------------------------------------------- 1 | export const requiredParam = (name) => { 2 | throw new Error(`Missing parameter '${name}'`); 3 | }; 4 | --------------------------------------------------------------------------------