From 76dc8be10f8ad328cd4574a06051d784aa48d9c0 Mon Sep 17 00:00:00 2001 From: trevorjtclarke Date: Fri, 9 Apr 2021 20:21:09 -0700 Subject: [PATCH] added tons of CLI code, all untested --- cli/bin/near => bin/crond | 6 +- bin/crond.js | 97 +++++++++ cli/bin/near-cli.js | 267 ------------------------- cli/{utils => }/check-version.js | 4 +- cli/commands/add-key.js | 41 ---- cli/commands/call.js | 53 ----- cli/commands/create-account.js | 154 -------------- cli/commands/delete-key.js | 23 --- cli/commands/dev-deploy.js | 96 --------- cli/commands/evm-call.js | 57 ------ cli/commands/evm-dev-init.js | 26 --- cli/commands/evm-view.js | 42 ---- cli/commands/generate-key.js | 45 ----- cli/commands/proposals.js | 12 -- cli/commands/tx-status.js | 38 ---- cli/commands/validators.js | 29 --- cli/commands/view-state.js | 45 ----- cli/config.js | 66 ------ cli/get-config.js | 19 -- cli/index.js | 229 ++------------------- cli/middleware/abi.js | 11 - cli/middleware/initial-balance.js | 6 - cli/middleware/key-store.js | 22 -- cli/middleware/ledger.js | 50 ----- cli/middleware/seed-phrase.js | 23 --- cli/{middleware => }/print-options.js | 0 cli/test/index.sh | 18 -- cli/test/res/fungible_token.wasm | Bin 169783 -> 0 bytes cli/test/test_account_creation.sh | 21 -- cli/test/test_account_operations.sh | 20 -- cli/test/test_contract.sh | 45 ----- cli/test/test_deploy_init_contract.sh | 38 ---- cli/test/test_generate_key.sh | 31 --- cli/test/unit/explorer.test.js | 27 --- cli/test/unit/inspect-response.test.js | 20 -- cli/utils/capture-login-success.js | 136 ------------- cli/utils/connect.js | 6 - cli/utils/exit-on-error.js | 42 ---- cli/utils/explorer.js | 22 -- cli/utils/implicit-accountid.js | 5 - cli/utils/inspect-response.js | 96 --------- cli/utils/readline.js | 37 ---- cli/utils/settings.js | 45 ----- cli/utils/validators-info.js | 96 --------- cli/utils/verify-account.js | 41 ---- src/actions.js | 121 +++++++++++ src/index.js | 105 +--------- 47 files changed, 239 insertions(+), 2194 deletions(-) rename cli/bin/near => bin/crond (66%) mode change 100755 => 100644 create mode 100644 bin/crond.js delete mode 100644 cli/bin/near-cli.js rename cli/{utils => }/check-version.js (91%) delete mode 100644 cli/commands/add-key.js delete mode 100644 cli/commands/call.js delete mode 100644 cli/commands/create-account.js delete mode 100644 cli/commands/delete-key.js delete mode 100644 cli/commands/dev-deploy.js delete mode 100644 cli/commands/evm-call.js delete mode 100644 cli/commands/evm-dev-init.js delete mode 100644 cli/commands/evm-view.js delete mode 100644 cli/commands/generate-key.js delete mode 100644 cli/commands/proposals.js delete mode 100644 cli/commands/tx-status.js delete mode 100644 cli/commands/validators.js delete mode 100644 cli/commands/view-state.js delete mode 100644 cli/config.js delete mode 100644 cli/get-config.js delete mode 100644 cli/middleware/abi.js delete mode 100644 cli/middleware/initial-balance.js delete mode 100644 cli/middleware/key-store.js delete mode 100644 cli/middleware/ledger.js delete mode 100644 cli/middleware/seed-phrase.js rename cli/{middleware => }/print-options.js (100%) delete mode 100755 cli/test/index.sh delete mode 100755 cli/test/res/fungible_token.wasm delete mode 100755 cli/test/test_account_creation.sh delete mode 100755 cli/test/test_account_operations.sh delete mode 100755 cli/test/test_contract.sh delete mode 100755 cli/test/test_deploy_init_contract.sh delete mode 100755 cli/test/test_generate_key.sh delete mode 100644 cli/test/unit/explorer.test.js delete mode 100644 cli/test/unit/inspect-response.test.js delete mode 100644 cli/utils/capture-login-success.js delete mode 100644 cli/utils/connect.js delete mode 100644 cli/utils/exit-on-error.js delete mode 100644 cli/utils/explorer.js delete mode 100644 cli/utils/implicit-accountid.js delete mode 100644 cli/utils/inspect-response.js delete mode 100644 cli/utils/readline.js delete mode 100644 cli/utils/settings.js delete mode 100644 cli/utils/validators-info.js delete mode 100644 cli/utils/verify-account.js create mode 100644 src/actions.js diff --git a/cli/bin/near b/bin/crond old mode 100755 new mode 100644 similarity index 66% rename from cli/bin/near rename to bin/crond index 813848f..ebced40 --- a/cli/bin/near +++ b/bin/crond @@ -5,13 +5,13 @@ require('v8flags')((e, flags) => { throw e; } flaggedRespawn( - flags.concat(['--experimental_repl_await']), - process.argv.indexOf('repl') == -1 ? process.argv : process.argv.concat(['--experimental-repl-await']), + flags, + process.argv, ready => { if (ready) { // Need to filter out '--no-respawning' to avoid yargs complaining about it process.argv = process.argv.filter(arg => arg != '--no-respawning'); - require('./near-cli.js'); + require('./crond.js'); } }); }) diff --git a/bin/crond.js b/bin/crond.js new file mode 100644 index 0000000..78746e8 --- /dev/null +++ b/bin/crond.js @@ -0,0 +1,97 @@ +require('dotenv').config() +const chalk = require('chalk') +const yargs = require('yargs') +import { agentFunction } from '../src/actions' + +const chalk = require('chalk'); + +const registerAgent = { + command: 'register ', + desc: 'Add your agent to cron known agents', + builder: (yargs) => yargs + .option('accountId', { + desc: 'Account to add', + type: 'string', + required: true + }) + .option('payableAccountId', { + desc: 'Account that receives reward payouts', + type: 'string', + required: false + }), + handler: async function (options) { + await agentFunction('register_agent', options); + } +}; + +const updateAgent = { + command: 'update ', + desc: 'Update your agent to cron known agents', + builder: (yargs) => yargs + .option('accountId', { + desc: 'Account to add', + type: 'string', + required: true + }) + .option('payableAccountId', { + desc: 'Account that receives reward payouts', + type: 'string', + required: false + }), + handler: async function (options) { + await agentFunction('update_agent', options); + } +}; + +const unregisterAgent = { + command: 'unregister ', + desc: 'Account to remove from list of active agents.', + builder: (yargs) => yargs + .option('accountId', { + desc: 'Account to remove.', + type: 'string', + required: true + }), + handler: async function (options) { + await agentFunction('unregister_agent', options) + } +}; + +const withdrawBalance = { + command: 'update ', + desc: 'Withdraw all rewards earned for this account', + builder: (yargs) => yargs + .option('accountId', { + desc: 'Account that earned rewards.', + type: 'string', + required: true + }), + handler: async function (options) { + await agentFunction('withdraw_task_balance', options); + } +}; + +let config = require('../src/config').getConfig(process.env.NODE_ENV || 'development'); +yargs // eslint-disable-line + .strict() + .middleware(require('../cli/check-version')) + .scriptName('crond') + .option('verbose', { + desc: 'Prints out verbose output', + type: 'boolean', + alias: 'v', + default: false + }) + .middleware(require('../src/print-options')) + .command(registerAgent) + .command(updateAgent) + .command(unregisterAgent) + .command(withdrawBalance) + .config(config) + .showHelpOnFail(true) + .recommendCommands() + .demandCommand(1, chalk`Pass {bold --help} to see all available commands and options.`) + .usage(chalk`Usage: {bold $0 [options]}`) + .epilogue(chalk`More info: {bold https://cron.cat}`) + .wrap(null) + .argv; diff --git a/cli/bin/near-cli.js b/cli/bin/near-cli.js deleted file mode 100644 index 6c3d27b..0000000 --- a/cli/bin/near-cli.js +++ /dev/null @@ -1,267 +0,0 @@ -const yargs = require('yargs'); -const main = require('../'); -const exitOnError = require('../utils/exit-on-error'); -const chalk = require('chalk'); - -// For account: - -const login = { - command: 'login', - desc: 'logging in through NEAR protocol wallet', - builder: (yargs) => yargs - .option('walletUrl', { - desc: 'URL of wallet to use', - type: 'string', - required: false - }), - handler: exitOnError(main.login) -}; - -const viewAccount = { - command: 'state ', - desc: 'view account state', - builder: (yargs) => yargs - .option('accountId', { - desc: 'Account to view', - type: 'string', - required: true - }), - handler: exitOnError(main.viewAccount) -}; - -const deleteAccount = { - command: 'delete ', - desc: 'delete an account and transfer funds to beneficiary account.', - builder: (yargs) => yargs - .option('accountId', { - desc: 'Account to delete', - type: 'string', - required: true - }) - .option('beneficiaryId', { - desc: 'Account to transfer funds to', - type: 'string', - required: true - }), - handler: exitOnError(main.deleteAccount) -}; - -const keys = { - command: 'keys ', - desc: 'view account public keys', - builder: (yargs) => yargs - .option('accountId', { - desc: 'Account to view', - type: 'string', - required: true - }), - handler: exitOnError(main.keys) -}; - -const sendMoney = { - command: 'send ', - desc: 'send tokens to given receiver', - builder: (yargs) => yargs - .option('amount', { - desc: 'Amount of NEAR tokens to send', - type: 'string', - }), - handler: exitOnError(main.sendMoney) -}; - -const stake = { - command: 'stake [accountId] [stakingKey] [amount]', - desc: 'create staking transaction', - builder: (yargs) => yargs - .option('accountId', { - desc: 'Account to stake on', - type: 'string', - required: true, - }) - .option('stakingKey', { - desc: 'Public key to stake with (base58 encoded)', - type: 'string', - required: true, - }) - .option('amount', { - desc: 'Amount to stake', - type: 'string', - required: true, - }), - handler: exitOnError(main.stake) -}; - -// For contract: -const deploy = { - command: 'deploy [accountId] [wasmFile] [initFunction] [initArgs] [initGas] [initDeposit]', - // command: 'deploy', - desc: 'deploy your smart contract', - builder: (yargs) => yargs - .option('wasmFile', { - desc: 'Path to wasm file to deploy', - type: 'string', - default: './out/main.wasm' - }) - .option('initFunction', { - desc: 'Initialization method', - type: 'string' - }) - .option('initArgs', { - desc: 'Initialization arguments', - }) - .option('initGas', { - desc: 'Gas for initialization call', - type: 'number', - default: 100000000000000 - }) - .option('initDeposit', { - desc: 'Deposit in Ⓝ to send for initialization call', - type: 'string', - default: '0' - }) - .alias({ - 'accountId': ['account_id', 'contractName', 'contract_name'], - }), - handler: exitOnError(main.deploy) -}; - -const callViewFunction = { - command: 'view [args]', - desc: 'make smart contract call which can view state', - builder: (yargs) => yargs - .option('args', { - desc: 'Arguments to the view call, in JSON format (e.g. \'{"param_a": "value"}\')', - type: 'string', - default: null - }), - handler: exitOnError(main.callViewFunction) -}; - -const clean = { - command: 'clean', - desc: 'clean the build environment', - builder: (yargs) => yargs - .option('outDir', { - desc: 'build directory', - type: 'string', - default: './out' - }), - handler: exitOnError(main.clean) -}; - -let config = require('../get-config')(); -yargs // eslint-disable-line - .strict() - .middleware(require('../utils/check-version')) - .scriptName('near') - .option('nodeUrl', { - desc: 'NEAR node URL', - type: 'string', - default: config.nodeUrl - }) - .option('networkId', { - desc: 'NEAR network ID, allows using different keys based on network', - type: 'string', - default: config.networkId - }) - .option('helperUrl', { - desc: 'NEAR contract helper URL', - type: 'string', - }) - .option('keyPath', { - desc: 'Path to master account key', - type: 'string', - }) - .option('accountId', { - desc: 'Unique identifier for the account', - type: 'string', - }) - .option('useLedgerKey', { - desc: 'Use Ledger for signing with given HD key path', - type: 'string', - default: "44'/397'/0'/0'/1'" - }) - .options('seedPhrase', { - desc: 'Seed phrase mnemonic', - type: 'string', - required: false - }) - .options('seedPath', { - desc: 'HD path derivation', - type: 'string', - default: "m/44'/397'/0'", - required: false - }) - .option('walletUrl', { - desc: 'Website for NEAR Wallet', - type: 'string' - }) - .option('contractName', { - desc: 'Account name of contract', - type: 'string' - }) - .option('masterAccount', { - desc: 'Master account used when creating new accounts', - type: 'string' - }) - .option('helperAccount', { - desc: 'Expected top-level account for a network', - type: 'string' - }) - .option('explorerUrl', { - hidden: true, - desc: 'Base url for explorer', - type: 'string', - }) - .option('verbose', { - desc: 'Prints out verbose output', - type: 'boolean', - alias: 'v', - default: false - }) - .middleware(require('../middleware/initial-balance')) - .middleware(require('../middleware/print-options')) - .middleware(require('../middleware/key-store')) - .middleware(require('../middleware/ledger')) - .middleware(require('../middleware/abi')) - .middleware(require('../middleware/seed-phrase')) - .command(require('../commands/create-account').createAccountCommand) - .command(require('../commands/create-account').createAccountCommandDeprecated) - .command(viewAccount) - .command(deleteAccount) - .command(keys) - .command(require('../commands/tx-status')) - .command(deploy) - .command(require('../commands/dev-deploy')) - .command(require('../commands/call')) - .command(callViewFunction) - .command(require('../commands/view-state')) - .command(sendMoney) - .command(clean) - .command(stake) - .command(login) - .command(require('../commands/repl')) - .command(require('../commands/generate-key')) - .command(require('../commands/add-key')) - .command(require('../commands/delete-key')) - .command(require('../commands/validators')) - .command(require('../commands/proposals')) - .command(require('../commands/evm-call')) - .command(require('../commands/evm-dev-init')) - .command(require('../commands/evm-view')) - .config(config) - .alias({ - 'accountId': ['account_id'], - 'nodeUrl': 'node_url', - 'networkId': ['network_id'], - 'wasmFile': 'wasm_file', - 'projectDir': 'project_dir', - 'outDir': 'out_dir' - }) - .showHelpOnFail(true) - .recommendCommands() - .demandCommand(1, chalk`Pass {bold --help} to see all available commands and options.`) - .usage(chalk`Usage: {bold $0 [options]}`) - .epilogue(chalk`Check out our epic whiteboard series: {bold http://near.ai/wbs}`) - .wrap(null) - .argv; diff --git a/cli/utils/check-version.js b/cli/check-version.js similarity index 91% rename from cli/utils/check-version.js rename to cli/check-version.js index a2d691d..0e82ed7 100644 --- a/cli/utils/check-version.js +++ b/cli/check-version.js @@ -11,7 +11,7 @@ const isCI = require('is-ci'); // avoid output if running in CI server const UPDATE_CHECK_INTERVAL_SECONDS = 1; /** - check the current version of NEAR CLI against latest as published on npm + check the current version of CROND CLI against latest as published on npm */ module.exports = async function checkVersion() { const pkg = require('../package.json'); @@ -26,7 +26,7 @@ module.exports = async function checkVersion() { const { type: diff, current, latest } = notifier.update; const update = normalizePhrasingOf(diff); const updateCommand = '{updateCommand}'; - const message = chalk`NEAR CLI has a ${update} available {dim ${current}} → {green ${latest}} + const message = chalk`CROND CLI has a ${update} available {dim ${current}} → {green ${latest}} Run {cyan ${updateCommand}} to avoid unexpected behavior`; const boxenOpts = { diff --git a/cli/commands/add-key.js b/cli/commands/add-key.js deleted file mode 100644 index bafb499..0000000 --- a/cli/commands/add-key.js +++ /dev/null @@ -1,41 +0,0 @@ -const exitOnError = require('../utils/exit-on-error'); -const connect = require('../utils/connect'); -const inspectResponse = require('../utils/inspect-response'); -const { utils } = require('near-api-js'); - - -module.exports = { - command: 'add-key ', - desc: 'Add an access key to given account', - builder: (yargs) => yargs - .option('access-key', { - desc: 'Public key to add (base58 encoded)', - type: 'string', - required: true, - }) - .option('contract-id', { - desc: 'Limit access key to given contract (if not provided - will create full access key)', - type: 'string', - required: false, - }) - .option('method-names', { - desc: 'Method names to limit access key to (example: --method-names meth1 meth2)', - type: 'array', - required: false, - }) - .option('allowance', { - desc: 'Allowance in $NEAR for the key (default 0)', - type: 'string', - required: false, - }), - handler: exitOnError(addAccessKey) -}; - -async function addAccessKey(options) { - console.log(`Adding ${options.contractId ? 'function call access' : 'full access'} key = ${options.accessKey} to ${options.accountId}.`); - const near = await connect(options); - const account = await near.account(options.accountId); - const allowance = utils.format.parseNearAmount(options.allowance); - const result = await account.addKey(options.accessKey, options.contractId, options.methodNames, allowance); - inspectResponse.prettyPrintResponse(result, options); -} diff --git a/cli/commands/call.js b/cli/commands/call.js deleted file mode 100644 index 904bb34..0000000 --- a/cli/commands/call.js +++ /dev/null @@ -1,53 +0,0 @@ -const { providers, utils } = require('near-api-js'); -const exitOnError = require('../utils/exit-on-error'); -const connect = require('../utils/connect'); -const inspectResponse = require('../utils/inspect-response'); - -module.exports = { - command: 'call [args]', - desc: 'schedule smart contract call which can modify state', - builder: (yargs) => yargs - .option('gas', { - desc: 'Max amount of gas this call can use (in gas units)', - type: 'string', - default: '100000000000000' - }) - .option('amount', { - desc: 'Number of tokens to attach (in NEAR)', - type: 'string', - default: '0' - }) - .option('base64', { - desc: 'Treat arguments as base64-encoded BLOB.', - type: 'boolean', - default: false - }) - .option('args', { - desc: 'Arguments to the contract call, in JSON format by default (e.g. \'{"param_a": "value"}\')', - type: 'string', - default: null - }) - .option('accountId', { - required: true, - desc: 'Unique identifier for the account that will be used to sign this call', - type: 'string', - }), - handler: exitOnError(scheduleFunctionCall) -}; - -async function scheduleFunctionCall(options) { - console.log(`Scheduling a call: ${options.contractName}.${options.methodName}(${options.args || ''})` + - (options.amount && options.amount != '0' ? ` with attached ${options.amount} NEAR` : '')); - const near = await connect(options); - const account = await near.account(options.accountId); - const parsedArgs = options.base64 ? Buffer.from(options.args, 'base64') : JSON.parse(options.args || '{}'); - const functionCallResponse = await account.functionCall( - options.contractName, - options.methodName, - parsedArgs, - options.gas, - utils.format.parseNearAmount(options.amount)); - const result = providers.getTransactionLastResult(functionCallResponse); - inspectResponse.prettyPrintResponse(functionCallResponse, options); - console.log(inspectResponse.formatResponse(result)); -} diff --git a/cli/commands/create-account.js b/cli/commands/create-account.js deleted file mode 100644 index 971b126..0000000 --- a/cli/commands/create-account.js +++ /dev/null @@ -1,154 +0,0 @@ - -const exitOnError = require('../utils/exit-on-error'); -const connect = require('../utils/connect'); -const { KeyPair } = require('near-api-js'); -const inspectResponse = require('../utils/inspect-response'); -// Top-level account (TLA) is testnet for foo.alice.testnet -const TLA_MIN_LENGTH = 32; - -const createAccountCommand = { - command: 'create-account ', - desc: 'create a new developer account (subaccount of the masterAccount, ex: app.alice.test)', - builder: (yargs) => yargs - .option('accountId', { - desc: 'Unique identifier for the newly created account', - type: 'string', - required: true - }) - .option('masterAccount', { - desc: 'Account used to create requested account.', - type: 'string', - required: true - }) - .option('publicKey', { - desc: 'Public key to initialize the account with', - type: 'string', - required: false - }) - .option('newLedgerKey', { - desc: 'HD key path to use with Ledger. Used to generate public key if not specified directly', - type: 'string', - default: "44'/397'/0'/0'/1'" - }) - .option('initialBalance', { - desc: 'Number of tokens to transfer to newly created account', - type: 'string', - default: '100' - }), - handler: exitOnError(createAccount) -}; - -const createAccountCommandDeprecated = { - command: 'create_account ', - builder: (yargs) => yargs - .option('accountId', { - desc: 'Unique identifier for the newly created account', - type: 'string', - required: true - }) - .option('masterAccount', { - desc: 'Account used to create requested account.', - type: 'string', - required: true - }) - .option('publicKey', { - desc: 'Public key to initialize the account with', - type: 'string', - required: false - }) - .option('newLedgerKey', { - desc: 'HD key path to use with Ledger. Used to generate public key if not specified directly', - type: 'string', - default: "44'/397'/0'/0'/1'" - }) - .option('initialBalance', { - desc: 'Number of tokens to transfer to newly created account', - type: 'string', - default: '100' - }), - handler: exitOnError(async (options) => { - console.log('near create_account is deprecated and will be removed in version 0.26.0. Please use near create-account.'); - await createAccount(options); }) -}; - -async function createAccount(options) { - // NOTE: initialBalance is passed as part of config here, parsed in middleware/initial-balance - // periods are disallowed in top-level accounts and can only be used for subaccounts - const splitAccount = options.accountId.split('.'); - - const splitMaster = options.masterAccount.split('.'); - const masterRootTLA = splitMaster[splitMaster.length - 1]; - if (splitAccount.length === 1) { - // TLA (bob-with-at-least-maximum-characters) - if (splitAccount[0].length < TLA_MIN_LENGTH) { - throw new Error(`Top-level accounts must be at least ${TLA_MIN_LENGTH} characters.\n` + - 'Note: this is for advanced usage only. Typical account names are of the form:\n' + - 'app.alice.test, where the masterAccount shares the top-level account (.test).' - ); - } - } else if (splitAccount.length > 1) { - // Subaccounts (short.alice.near, even.more.bob.test, and eventually peter.potato) - // Check that master account TLA matches - if (!options.accountId.endsWith(`.${options.masterAccount}`)) { - throw new Error(`New account doesn't share the same top-level account. Expecting account name to end in ".${options.masterAccount}"`); - } - - // Warn user if account seems to be using wrong network, where TLA is captured in config - // TODO: when "network" key is available, revisit logic to determine if user is on proper network - // See: https://github.com/near/near-cli/issues/387 - if (options.helperAccount && masterRootTLA !== options.helperAccount) { - console.log(`NOTE: In most cases, when connected to network "${options.networkId}", masterAccount will end in ".${options.helperAccount}"`); - } - } - let near = await connect(options); - let keyPair; - let publicKey; - let keyRootPath; - let keyFilePath; - if (options.publicKey) { - publicKey = options.publicKey; - } else { - keyPair = await KeyPair.fromRandom('ed25519'); - publicKey = keyPair.getPublicKey(); - } - // Check to see if account already exists - try { - // This is expected to error because the account shouldn't exist - const account = await near.account(options.accountId); - await account.state(); - throw new Error(`Sorry, account '${options.accountId}' already exists.`); - } catch (e) { - if (!e.message.includes('does not exist while viewing')) { - throw e; - } - } - if (keyPair) { - if (near.connection.signer.keyStore.keyStores.length) { - keyRootPath = near.connection.signer.keyStore.keyStores[0].keyDir; - } - keyFilePath = `${keyRootPath}/${options.networkId}/${options.accountId}.json`; - await near.connection.signer.keyStore.setKey(options.networkId, options.accountId, keyPair); - } - // Create account - console.log(`Saving key to '${keyFilePath}'`); - try { - const response = await near.createAccount(options.accountId, publicKey); - inspectResponse.prettyPrintResponse(response, options); - console.log(`Account ${options.accountId} for network "${options.networkId}" was created.`); - - } catch(error) { - if (error.type === 'RetriesExceeded') { - console.warn('Received a timeout when creating account, please run:'); - console.warn(`near state ${options.accountId}`); - console.warn('to confirm creation. Keyfile for this account has been saved.'); - } else { - if (!options.usingLedger) await near.connection.signer.keyStore.removeKey(options.networkId, options.accountId); - throw error; - } - } -} - -module.exports = { - createAccountCommand, - createAccountCommandDeprecated -}; diff --git a/cli/commands/delete-key.js b/cli/commands/delete-key.js deleted file mode 100644 index 380c771..0000000 --- a/cli/commands/delete-key.js +++ /dev/null @@ -1,23 +0,0 @@ -const exitOnError = require('../utils/exit-on-error'); -const connect = require('../utils/connect'); -const inspectResponse = require('../utils/inspect-response'); - -module.exports = { - command: 'delete-key ', - desc: 'delete access key', - builder: (yargs) => yargs - .option('access-key', { - desc: 'Public key to delete (base58 encoded)', - type: 'string', - required: true, - }), - handler: exitOnError(deleteAccessKey) -}; - -async function deleteAccessKey(options) { - console.log(`Deleting key = ${options.accessKey} on ${options.accountId}.`); - const near = await connect(options); - const account = await near.account(options.accountId); - const result = await account.deleteKey(options.accessKey); - inspectResponse.prettyPrintResponse(result, options); -} diff --git a/cli/commands/dev-deploy.js b/cli/commands/dev-deploy.js deleted file mode 100644 index 99e6c0b..0000000 --- a/cli/commands/dev-deploy.js +++ /dev/null @@ -1,96 +0,0 @@ -const { KeyPair } = require('near-api-js'); -const exitOnError = require('../utils/exit-on-error'); -const connect = require('../utils/connect'); -const { readFile, writeFile, mkdir } = require('fs').promises; -const { existsSync } = require('fs'); - -const { PROJECT_KEY_DIR } = require('../middleware/key-store'); - -const eventtracking = require('../utils/eventtracking'); -const inspectResponse = require('../utils/inspect-response'); - - -module.exports = { - command: 'dev-deploy [wasmFile]', - desc: 'deploy your smart contract using temporary account (TestNet only)', - builder: (yargs) => yargs - .option('wasmFile', { - desc: 'Path to wasm file to deploy', - type: 'string', - default: './out/main.wasm' - }) - .option('init', { - desc: 'Create new account for deploy (even if there is one already available)', - type: 'boolean', - default: false - }) - .option('initialBalance', { - desc: 'Number of tokens to transfer to newly created account', - type: 'string', - default: '100' - }) - .alias({ - 'init': ['force', 'f'], - }), - handler: exitOnError(devDeploy) -}; - -async function devDeploy(options) { - await eventtracking.askForConsentIfNeeded(options); - const { nodeUrl, helperUrl, masterAccount, wasmFile } = options; - if (!helperUrl && !masterAccount) { - throw new Error('Cannot create account as neither helperUrl nor masterAccount is specified in config for current NODE_ENV (see src/config.js)'); - } - const near = await connect(options); - const accountId = await createDevAccountIfNeeded({ ...options, near }); - console.log( - `Starting deployment. Account id: ${accountId}, node: ${nodeUrl}, helper: ${helperUrl}, file: ${wasmFile}`); - const contractData = await readFile(wasmFile); - const account = await near.account(accountId); - const result = await account.deployContract(contractData); - inspectResponse.prettyPrintResponse(result, options); - console.log(`Done deploying to ${accountId}`); -} - -async function createDevAccountIfNeeded({ near, keyStore, networkId, init, masterAccount }) { - // TODO: once examples and create-near-app use the dev-account.env file, we can remove the creation of dev-account - // https://github.com/near/near-cli/issues/287 - const accountFilePath = `${PROJECT_KEY_DIR}/dev-account`; - const accountFilePathEnv = `${PROJECT_KEY_DIR}/dev-account.env`; - if (!init) { - try { - // throws if either file is missing - const existingAccountId = (await readFile(accountFilePath)).toString('utf8').trim(); - await readFile(accountFilePathEnv); - if (existingAccountId && await keyStore.getKey(networkId, existingAccountId)) { - return existingAccountId; - } - } catch (e) { - if (e.code === 'ENOENT') { - // Create neardev directory, new account will be created below - if (!existsSync(PROJECT_KEY_DIR)) { - await mkdir(PROJECT_KEY_DIR); - } - } else { - throw e; - } - } - } - let accountId; - // create random number with at least 7 digits - const randomNumber = Math.floor(Math.random() * (9999999 - 1000000) + 1000000); - - if (masterAccount) { - accountId = `dev-${Date.now()}.${masterAccount}`; - } else { - accountId = `dev-${Date.now()}-${randomNumber}`; - } - - const keyPair = await KeyPair.fromRandom('ed25519'); - await near.accountCreator.createAccount(accountId, keyPair.publicKey); - await keyStore.setKey(networkId, accountId, keyPair); - await writeFile(accountFilePath, accountId); - // write file to be used by env-cmd - await writeFile(accountFilePathEnv, `CONTRACT_NAME=${accountId}`); - return accountId; -} diff --git a/cli/commands/evm-call.js b/cli/commands/evm-call.js deleted file mode 100644 index aeb3000..0000000 --- a/cli/commands/evm-call.js +++ /dev/null @@ -1,57 +0,0 @@ -const exitOnError = require('../utils/exit-on-error'); -const web3 = require('web3'); -const { NearProvider, utils } = require('near-web3-provider'); -const assert = require('assert'); - -module.exports = { - command: 'evm-call [args]', - desc: 'Schedule call inside EVM machine', - builder: (yargs) => yargs - .option('gas', { - desc: 'Max amount of NEAR gas this call can use', - type: 'string', - default: '100000000000000' - }) - .option('amount', { - desc: 'Number of tokens to attach', - type: 'string', - default: '0' - }) - .option('args', { - desc: 'Arguments to the contract call, in JSON format (e.g. \'[1, "str"]\') based on contract ABI', - type: 'string', - default: null - }) - .option('accountId', { - required: true, - desc: 'Unique identifier for the account that will be used to sign this call', - type: 'string', - }) - .option('abi', { - required: true, - desc: 'Path to ABI for given contract', - type: 'string', - }), - handler: exitOnError(scheduleEVMFunctionCall) -}; - -async function scheduleEVMFunctionCall(options) { - const args = JSON.parse(options.args || '[]'); - console.log(`Scheduling a call inside ${options.evmAccount} EVM:`); - console.log(`${options.contractName}.${options.methodName}()` + - (options.amount && options.amount !== '0' ? ` with attached ${options.amount} NEAR` : '')); - console.log(' with args', args); - const web = new web3(); - web.setProvider(new NearProvider({ - nodeUrl: options.nodeUrl, - // TODO: make sure near-api-js has the same version between near-web3-provider. - // keyStore: options.keyStore, - masterAccountId: options.accountId, - networkId: options.networkId, - evmAccountId: options.evmAccount, - keyPath: options.keyPath, - })); - const contract = new web.eth.Contract(options.abi, options.contractName); - assert(options.methodName in contract.methods, `${options.methodName} is not present in ABI`); - await contract.methods[options.methodName](...args).send({ from: utils.nearAccountToEvmAddress(options.accountId) }); -} diff --git a/cli/commands/evm-dev-init.js b/cli/commands/evm-dev-init.js deleted file mode 100644 index 16b3bd0..0000000 --- a/cli/commands/evm-dev-init.js +++ /dev/null @@ -1,26 +0,0 @@ -const exitOnError = require('../utils/exit-on-error'); -const { utils } = require('near-web3-provider'); -const connect = require('../utils/connect'); - -module.exports = { - command: 'evm-dev-init [numAccounts]', - desc: 'Creates test accounts using NEAR Web3 Provider', - builder: (yargs) => yargs - .option('accountId', { - desc: 'NEAR account creating the test subaccounts', - type: 'string', - default: '0' - }) - .option('numAccounts', { - desc: 'Number of test accounts to create', - type: 'number', - default: '5' - }), - handler: exitOnError(scheduleEVMDevInit) -}; - -async function scheduleEVMDevInit(options) { - const near = await connect(options); - const account = await near.account(options.accountId); - await utils.createTestAccounts(account, options.numAccounts); -} diff --git a/cli/commands/evm-view.js b/cli/commands/evm-view.js deleted file mode 100644 index a197da7..0000000 --- a/cli/commands/evm-view.js +++ /dev/null @@ -1,42 +0,0 @@ -const exitOnError = require('../utils/exit-on-error'); -const web3 = require('web3'); -const { NearProvider, utils } = require('near-web3-provider'); -const assert = require('assert'); - -module.exports = { - command: 'evm-view [args]', - desc: 'View call inside EVM machine', - builder: (yargs) => yargs - .option('args', { - desc: 'Arguments to the contract call, in JSON format (e.g. \'[1, "str"]\') based on contract ABI', - type: 'string', - default: null - }) - .option('accountId', { - required: true, - desc: 'Unique identifier for the account that will be used to sign this call', - type: 'string', - }) - .option('abi', { - desc: 'Path to ABI for given contract', - type: 'string', - }), - handler: exitOnError(scheduleEVMFunctionView) -}; - -async function scheduleEVMFunctionView(options) { - const web = new web3(); - web.setProvider(new NearProvider({ - nodeUrl: options.nodeUrl, - // TODO: make sure near-api-js has the same version between near-web3-provider. - // keyStore: options.keyStore, - masterAccountId: options.accountId, - networkId: options.networkId, - evmAccountId: options.evmAccount, - })); - const contract = new web.eth.Contract(options.abi, options.contractName); - const args = JSON.parse(options.args || '[]'); - assert(options.methodName in contract.methods, `${options.methodName} is not present in ABI`); - const result = await contract.methods[options.methodName](...args).call({ from: utils.nearAccountToEvmAddress(options.accountId) }); - console.log(result); -} diff --git a/cli/commands/generate-key.js b/cli/commands/generate-key.js deleted file mode 100644 index 7e0f4ee..0000000 --- a/cli/commands/generate-key.js +++ /dev/null @@ -1,45 +0,0 @@ -const KeyPair = require('near-api-js').KeyPair; -const exitOnError = require('../utils/exit-on-error'); -const implicitAccountId = require('../utils/implicit-accountid'); - -module.exports = { - command: 'generate-key [account-id]', - desc: 'generate key or show key from Ledger', - builder: (yargs) => yargs, - handler: exitOnError(async (argv) => { - let near = await require('../utils/connect')(argv); - - if (argv.usingLedger) { - if (argv.accountId) { - console.log('WARN: Account id is provided but ignored in case of using Ledger.'); - } - const publicKey = await argv.signer.getPublicKey(); - // NOTE: Command above already prints public key. - console.log(`Implicit account: ${implicitAccountId(publicKey.toString())}`); - // TODO: query all accounts with this public key here. - // TODO: check if implicit account exist, and if the key doen't match already. - return; - } - - const { deps: { keyStore } } = near.config; - const existingKey = await keyStore.getKey(argv.networkId, argv.accountId); - if (existingKey) { - console.log(`Account has existing key pair with ${existingKey.publicKey} public key`); - return; - } - - // If key doesn't exist, create one and store in the keyStore. - // Otherwise, it's expected that both key and accountId are already provided in arguments. - if (!argv.publicKey) { - const keyPair = KeyPair.fromRandom('ed25519'); - argv.publicKey = keyPair.publicKey.toString(); - argv.accountId = argv.accountId || implicitAccountId(argv.publicKey); - await keyStore.setKey(argv.networkId, argv.accountId, keyPair); - } else if (argv.seedPhrase) { - const seededKeyPair = await argv.signer.keyStore.getKey(argv.networkId, argv.accountId); - await keyStore.setKey(argv.networkId, argv.accountId, seededKeyPair); - } - - console.log(`Key pair with ${argv.publicKey} public key for an account "${argv.accountId}"`); - }) -}; \ No newline at end of file diff --git a/cli/commands/proposals.js b/cli/commands/proposals.js deleted file mode 100644 index dcad3e4..0000000 --- a/cli/commands/proposals.js +++ /dev/null @@ -1,12 +0,0 @@ -const exitOnError = require('../utils/exit-on-error'); -const connect = require('../utils/connect'); -const validatorsInfo = require('../utils/validators-info'); - -module.exports = { - command: 'proposals', - desc: 'show both new proposals in the current epoch as well as current validators who are implicitly proposing', - handler: exitOnError(async (argv) => { - const near = await connect(argv); - await validatorsInfo.showProposalsTable(near); - }) -}; diff --git a/cli/commands/tx-status.js b/cli/commands/tx-status.js deleted file mode 100644 index 33b15ed..0000000 --- a/cli/commands/tx-status.js +++ /dev/null @@ -1,38 +0,0 @@ -const exitOnError = require('../utils/exit-on-error'); -const connect = require('../utils/connect'); -const inspectResponse = require('../utils/inspect-response'); -const bs58 = require('bs58'); - -module.exports = { - command: 'tx-status ', - desc: 'lookup transaction status by hash', - builder: (yargs) => yargs - .option('hash', { - desc: 'base58-encoded hash', - type: 'string', - required: true - }), - handler: exitOnError(async (argv) => { - const near = await connect(argv); - - const hashParts = argv.hash.split(':'); - let hash, accountId; - if (hashParts.length == 2) { - [accountId, hash] = hashParts; - } else if (hashParts.length == 1) { - [hash] = hashParts; - } else { - throw new Error('Unexpected transaction hash format'); - } - accountId = accountId || argv.accountId || argv.masterAccount; - - if (!accountId) { - throw new Error('Please specify account id, either as part of transaction hash or using --accountId flag.'); - } - - const status = await near.connection.provider.txStatus(bs58.decode(hash), accountId); - console.log(`Transaction ${accountId}:${hash}`); - console.log(inspectResponse.formatResponse(status)); - - }) -}; diff --git a/cli/commands/validators.js b/cli/commands/validators.js deleted file mode 100644 index 8d2e05c..0000000 --- a/cli/commands/validators.js +++ /dev/null @@ -1,29 +0,0 @@ -const exitOnError = require('../utils/exit-on-error'); -const connect = require('../utils/connect'); -const validatorsInfo = require('../utils/validators-info'); - -module.exports = { - command: 'validators ', - desc: 'lookup validators for given epoch (or current / next)', - builder: (yargs) => yargs - .option('epoch', { - desc: 'epoch defined by block number or current / next', - type: 'string', - required: true - }), - handler: exitOnError(async (argv) => { - const near = await connect(argv); - - switch (argv.epoch) { - case 'current': - await validatorsInfo.showValidatorsTable(near, null); - break; - case 'next': - await validatorsInfo.showNextValidatorsTable(near); - break; - default: - await validatorsInfo.showValidatorsTable(near, argv.epoch); - break; - } - }) -}; diff --git a/cli/commands/view-state.js b/cli/commands/view-state.js deleted file mode 100644 index fec9944..0000000 --- a/cli/commands/view-state.js +++ /dev/null @@ -1,45 +0,0 @@ -const exitOnError = require('../utils/exit-on-error'); -const connect = require('../utils/connect'); -const { formatResponse } = require('../utils/inspect-response'); - -module.exports = { - command: 'view-state [prefix]', - desc: 'View contract storage state', - builder: (yargs) => yargs - .option('prefix', { - desc: 'Return keys only with given prefix.', - type: 'string', - default: '' - - }) - .option('block-id', { - desc: 'The block number OR the block hash (base58-encoded).', - type: 'string', - - }) - .option('finality', { - desc: '`optimistic` uses the latest block recorded on the node that responded to your query,\n' + - '`final` is for a block that has been validated on at least 66% of the nodes in the network', - type: 'string', - choices: ['optimistic', 'final'], - - }) - .option('utf8', { - desc: 'Decode keys and values as UTF-8 strings', - type: 'boolean', - default: false - }), - handler: exitOnError(viewState) -}; - -async function viewState(options) { - const { accountId, prefix, finality, blockId, utf8 } = options; - const near = await connect(options); - const account = await near.account(accountId); - - let state = await account.viewState(prefix, { blockId, finality }); - if (utf8) { - state = state.map(({ key, value}) => ({ key: key.toString('utf-8'), value: value.toString('utf-8') })); - } - console.log(formatResponse(state, options)); -} diff --git a/cli/config.js b/cli/config.js deleted file mode 100644 index 84fc2ae..0000000 --- a/cli/config.js +++ /dev/null @@ -1,66 +0,0 @@ -const CONTRACT_NAME = process.env.CONTRACT_NAME; - -function getConfig(env) { - switch (env) { - - case 'production': - case 'mainnet': - return { - networkId: 'mainnet', - nodeUrl: 'https://rpc.mainnet.near.org', - contractName: CONTRACT_NAME, - walletUrl: 'https://wallet.near.org', - helperUrl: 'https://helper.mainnet.near.org', - helperAccount: 'near', - explorerUrl: 'https://explorer.mainnet.near.org', - }; - case 'development': - case 'testnet': - return { - networkId: 'default', - nodeUrl: 'https://rpc.testnet.near.org', - contractName: CONTRACT_NAME, - walletUrl: 'https://wallet.testnet.near.org', - helperUrl: 'https://helper.testnet.near.org', - helperAccount: 'testnet', - explorerUrl: 'https://explorer.testnet.near.org', - }; - case 'betanet': - return { - networkId: 'betanet', - nodeUrl: 'https://rpc.betanet.near.org', - contractName: CONTRACT_NAME, - walletUrl: 'https://wallet.betanet.near.org', - helperUrl: 'https://helper.betanet.near.org', - helperAccount: 'betanet', - explorerUrl: 'https://explorer.betanet.near.org', - }; - case 'local': - return { - networkId: 'local', - nodeUrl: 'http://localhost:3030', - keyPath: `${process.env.HOME}/.near/validator_key.json`, - walletUrl: 'http://localhost:4000/wallet', - contractName: CONTRACT_NAME, - }; - case 'test': - case 'ci': - return { - networkId: 'shared-test', - nodeUrl: 'https://rpc.ci-testnet.near.org', - contractName: CONTRACT_NAME, - masterAccount: 'test.near', - }; - case 'ci-betanet': - return { - networkId: 'shared-test-staging', - nodeUrl: 'https://rpc.ci-betanet.near.org', - contractName: CONTRACT_NAME, - masterAccount: 'test.near', - }; - default: - throw Error(`Unconfigured environment '${env}'. Can be configured in src/config.js.`); - } -} - -module.exports = getConfig; diff --git a/cli/get-config.js b/cli/get-config.js deleted file mode 100644 index 4b387d8..0000000 --- a/cli/get-config.js +++ /dev/null @@ -1,19 +0,0 @@ - -module.exports = function getConfig() { - const configPath = process.cwd() + '/src/config'; - const nearEnv = process.env.NEAR_ENV || process.env.NODE_ENV || 'development'; - try { - const config = require(configPath)(nearEnv); - return config; - } catch (e) { - if (e.code == 'MODULE_NOT_FOUND') { - if (process.env.NEAR_DEBUG) { - // TODO: Use debug module instead, see https://github.com/near/near-api-js/pull/250 - console.warn(`[WARNING] Didn't find config at ${configPath}, using default shell config`); - } - const defaultConfig = require('./config')(nearEnv); - return defaultConfig; - } - throw e; - } -}; diff --git a/cli/index.js b/cli/index.js index 34a1afb..35e2663 100644 --- a/cli/index.js +++ b/cli/index.js @@ -1,220 +1,17 @@ -const fs = require('fs'); -const yargs = require('yargs'); -const ncp = require('ncp').ncp; -ncp.limit = 16; -const rimraf = require('rimraf'); -const readline = require('readline'); -const URL = require('url').URL; -const qs = require('querystring'); -const chalk = require('chalk'); // colorize output -const open = require('open'); // open URL in default browser -const { KeyPair, utils, transactions } = require('near-api-js'); -const connect = require('./utils/connect'); -const verify = require('./utils/verify-account'); -const capture = require('./utils/capture-login-success'); +import { registerAgent, agentFunction } from '../src/actions' -const inspectResponse = require('./utils/inspect-response'); -const eventtracking = require('./utils/eventtracking'); +exports.registerAgent = async function (options) { + await registerAgent(options); +} -// TODO: Fix promisified wrappers to handle error properly +exports.unregisterAgent = async function (options) { + await agentFunction('unregister_agent', options); +} -// For smart contract: -exports.clean = async function () { - const rmDirFn = () => { - return new Promise(resolve => { - rimraf(yargs.argv.outDir, response => resolve(response)); - }); - }; - await rmDirFn(); - console.log('Clean complete.'); -}; +exports.updateAgent = async function (options) { + await agentFunction('update_agent', options); +} -exports.deploy = async function (options) { - console.log( - `Starting deployment. Account id: ${options.accountId}, node: ${options.nodeUrl}, helper: ${options.helperUrl}, file: ${options.wasmFile}`); - - const near = await connect(options); - const account = await near.account(options.accountId); - let prevState = await account.state(); - let prevCodeHash = prevState.code_hash; - // Deploy with init function and args - const txs = [transactions.deployContract(fs.readFileSync(options.wasmFile))]; - - if (options.initFunction) { - if (!options.initArgs) { - console.error('Must add initialization arguments.\nExample: near deploy --accountId near.testnet --initFunction "new" --initArgs \'{"key": "value"}\''); - await eventtracking.track(eventtracking.EVENT_ID_DEPLOY_END, { success: false, error: 'Must add initialization arguments' }, options); - process.exit(1); - } - txs.push(transactions.functionCall( - options.initFunction, - Buffer.from(options.initArgs), - options.initGas, - utils.format.parseNearAmount(options.initDeposit)), - ); - } - - const result = await account.signAndSendTransaction(options.accountId, txs); - inspectResponse.prettyPrintResponse(result, options); - let state = await account.state(); - let codeHash = state.code_hash; - await eventtracking.track(eventtracking.EVENT_ID_DEPLOY_END, { success: true, code_hash: codeHash, is_same_contract: prevCodeHash === codeHash, contract_id: options.accountId }, options); - eventtracking.trackDeployedContract(); - console.log(`Done deploying ${options.initFunction ? 'and initializing' : 'to'} ${options.accountId}`); -}; - -exports.callViewFunction = async function (options) { - console.log(`View call: ${options.contractName}.${options.methodName}(${options.args || ''})`); - const near = await connect(options); - const account = await near.account(options.accountId || options.masterAccount || options.contractName); - console.log(inspectResponse.formatResponse(await account.viewFunction(options.contractName, options.methodName, JSON.parse(options.args || '{}')))); -}; - -// open a given URL in browser in a safe way. -const openUrl = async function(url) { - try { - await open(url.toString()); - } catch (error) { - console.error(`Failed to open the URL [ ${url.toString()} ]`, error); - } -}; - -exports.login = async function (options) { - await eventtracking.askForConsentIfNeeded(options); - if (!options.walletUrl) { - console.log('Log in is not needed on this environment. Please use appropriate master account for shell operations.'); - await eventtracking.track(eventtracking.EVENT_ID_LOGIN_END, { success: true, login_is_not_needed: true }, options); - } else { - const newUrl = new URL(options.walletUrl + '/login/'); - const title = 'NEAR CLI'; - newUrl.searchParams.set('title', title); - const keyPair = await KeyPair.fromRandom('ed25519'); - newUrl.searchParams.set('public_key', keyPair.getPublicKey()); - - console.log(chalk`\n{bold.yellow Please authorize NEAR CLI} on at least one of your accounts.`); - - // attempt to capture accountId automatically via browser callback - let tempUrl; - const isWin = process.platform === 'win32'; - - // find a callback URL on the local machine - try { - if (!isWin) { // capture callback is currently not working on windows. This is a workaround to not use it - tempUrl = await capture.callback(5000); - } - } catch (error) { - // console.error("Failed to find suitable port.", error.message) - // TODO: Is it? Try triggering error - // silent error is better here - } - - // if we found a suitable URL, attempt to use it - if (tempUrl) { - if (process.env.GITPOD_WORKSPACE_URL) { - const workspaceUrl = new URL(process.env.GITPOD_WORKSPACE_URL); - newUrl.searchParams.set('success_url', `https://${tempUrl.port}-${workspaceUrl.hostname}`); - // Browser not opened, as will open automatically for opened port - } else { - newUrl.searchParams.set('success_url', `http://${tempUrl.hostname}:${tempUrl.port}`); - openUrl(newUrl); - } - } else if (isWin) { - // redirect automatically on windows, but do not use the browser callback - openUrl(newUrl); - } - - console.log(chalk`\n{dim If your browser doesn't automatically open, please visit this URL\n${newUrl.toString()}}`); - - const getAccountFromWebpage = async () => { - // capture account_id as provided by NEAR Wallet - const [accountId] = await capture.payload(['account_id'], tempUrl, newUrl); - return accountId; - }; - - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - const redirectAutomaticallyHint = tempUrl ? ' (if not redirected automatically)' : ''; - const getAccountFromConsole = async () => { - return await new Promise((resolve) => { - rl.question( - chalk`Please authorize at least one account at the URL above.\n\n` + - chalk`Which account did you authorize for use with NEAR CLI?\n` + - chalk`{bold Enter it here${redirectAutomaticallyHint}:}\n`, async (accountId) => { - resolve(accountId); - }); - }); - }; - - let accountId; - if (!tempUrl) { - accountId = await getAccountFromConsole(); - } else { - accountId = await new Promise((resolve, reject) => { - let resolved = false; - const resolveOnce = (result) => { if (!resolved) resolve(result); resolved = true; }; - getAccountFromWebpage() - .then(resolveOnce); // NOTE: error ignored on purpose - getAccountFromConsole() - .then(resolveOnce) - .catch(reject); - }); - } - rl.close(); - capture.cancel(); - // verify the accountId if we captured it or ... - try { - const success = await verify(accountId, keyPair, options); - await eventtracking.track(eventtracking.EVENT_ID_LOGIN_END, { success }, options); - } catch (error) { - await eventtracking.track(eventtracking.EVENT_ID_LOGIN_END, { success: false, error }, options); - console.error('Failed to verify accountId.', error.message); - } - } -}; - -exports.viewAccount = async function (options) { - let near = await connect(options); - let account = await near.account(options.accountId); - let state = await account.state(); - if (state && state.amount) { - state['formattedAmount'] = utils.format.formatNearAmount(state.amount); - } - console.log(`Account ${options.accountId}`); - console.log(inspectResponse.formatResponse(state)); -}; - -exports.deleteAccount = async function (options) { - console.log( - `Deleting account. Account id: ${options.accountId}, node: ${options.nodeUrl}, helper: ${options.helperUrl}, beneficiary: ${options.beneficiaryId}`); - const near = await connect(options); - const account = await near.account(options.accountId); - const result = await account.deleteAccount(options.beneficiaryId); - inspectResponse.prettyPrintResponse(result, options); - console.log(`Account ${options.accountId} for network "${options.networkId}" was deleted.`); -}; - -exports.keys = async function (options) { - let near = await connect(options); - let account = await near.account(options.accountId); - let accessKeys = await account.getAccessKeys(); - console.log(`Keys for account ${options.accountId}`); - console.log(inspectResponse.formatResponse(accessKeys)); -}; - -exports.sendMoney = async function (options) { - console.log(`Sending ${options.amount} NEAR to ${options.receiver} from ${options.sender}`); - const near = await connect(options); - const account = await near.account(options.sender); - const result = await account.sendMoney(options.receiver, utils.format.parseNearAmount(options.amount)); - inspectResponse.prettyPrintResponse(result, options); -}; - -exports.stake = async function (options) { - console.log(`Staking ${options.amount} (${utils.format.parseNearAmount(options.amount)}) on ${options.accountId} with public key = ${qs.unescape(options.stakingKey)}.`); - const near = await connect(options); - const account = await near.account(options.accountId); - const result = await account.stake(qs.unescape(options.stakingKey), utils.format.parseNearAmount(options.amount)); - inspectResponse.prettyPrintResponse(result, options); -}; +exports.withdrawBalance = async function (options) { + await agentFunction('withdraw_task_balance', options); +} diff --git a/cli/middleware/abi.js b/cli/middleware/abi.js deleted file mode 100644 index 900d265..0000000 --- a/cli/middleware/abi.js +++ /dev/null @@ -1,11 +0,0 @@ -const fs = require('fs'); - -async function loadAbi(abiPath) { - return JSON.parse(fs.readFileSync(abiPath)).abi; -} - -module.exports = async function parseAbi(options) { - if (options.abi) { - options.abi = await loadAbi(options.abi); - } -}; \ No newline at end of file diff --git a/cli/middleware/initial-balance.js b/cli/middleware/initial-balance.js deleted file mode 100644 index dd43f49..0000000 --- a/cli/middleware/initial-balance.js +++ /dev/null @@ -1,6 +0,0 @@ - -const { utils } = require('near-api-js'); - -module.exports = async function parseInitialBalance(options) { - options.initialBalance = utils.format.parseNearAmount(options.initialBalance); -}; \ No newline at end of file diff --git a/cli/middleware/key-store.js b/cli/middleware/key-store.js deleted file mode 100644 index 8fa03d7..0000000 --- a/cli/middleware/key-store.js +++ /dev/null @@ -1,22 +0,0 @@ -const { keyStores } = require('near-api-js'); -const homedir = require('os').homedir(); -const path = require('path'); -const MergeKeyStore = keyStores.MergeKeyStore; -const UnencryptedFileSystemKeyStore = keyStores.UnencryptedFileSystemKeyStore; - -const CREDENTIALS_DIR = '.near-credentials'; -const PROJECT_KEY_DIR = './neardev'; - -module.exports = async function createKeyStore() { - // ./neardev is an old way of storing keys under project folder. We want to fallback there for backwards compatibility - // TODO: use system keystores. - // TODO: setting in config for which keystore to use - const credentialsPath = path.join(homedir, CREDENTIALS_DIR); - const keyStores = [ - new UnencryptedFileSystemKeyStore(credentialsPath), - new UnencryptedFileSystemKeyStore(PROJECT_KEY_DIR) - ]; - return { keyStore: new MergeKeyStore(keyStores) }; -}; - -module.exports.PROJECT_KEY_DIR = PROJECT_KEY_DIR; \ No newline at end of file diff --git a/cli/middleware/ledger.js b/cli/middleware/ledger.js deleted file mode 100644 index c3eb9cc..0000000 --- a/cli/middleware/ledger.js +++ /dev/null @@ -1,50 +0,0 @@ -const { utils: { PublicKey, key_pair: { KeyType } } } = require('near-api-js'); - -// near ... --useLedgerKey -// near create_account new_account_name --newLedgerKey --useLedgerKey --masterAccount account_that_creates -module.exports = async function useLedgerSigner({ useLedgerKey: ledgerKeyPath, newLedgerKey, publicKey }, yargs) { - if (yargs.parsed.defaulted.useLedgerKey) { - // NOTE: This checks if --useLedgerKey was specified at all, default value still can be used - return; - } - - const { createClient } = require('near-ledger-js'); - const { default: TransportNodeHid } = require('@ledgerhq/hw-transport-node-hid'); - - console.log('Make sure to connect your Ledger and open NEAR app'); - const transport = await TransportNodeHid.create(); - const client = await createClient(transport); - - let cachedPublicKeys = {}; - async function getPublicKeyForPath(hdKeyPath) { - // NOTE: Public key is cached to avoid confirming on Ledger multiple times - if (cachedPublicKeys[ledgerKeyPath]) { - return cachedPublicKeys[hdKeyPath]; - } - - console.log('Waiting for confirmation on Ledger...'); - const rawPublicKey = await client.getPublicKey(hdKeyPath); - const publicKey = new PublicKey({ keyType: KeyType.ED25519, data: rawPublicKey }); - cachedPublicKeys[hdKeyPath] = publicKey; - console.log('Using public key:', publicKey.toString()); - return publicKey; - } - - let signer = { - async getPublicKey() { - return getPublicKeyForPath(ledgerKeyPath); - }, - async signMessage(message) { - const publicKey = await getPublicKeyForPath(ledgerKeyPath); - console.log('Waiting for confirmation on Ledger...'); - const signature = await client.sign(message, ledgerKeyPath); - return { signature, publicKey }; - } - }; - - if (newLedgerKey) { - publicKey = await getPublicKeyForPath(newLedgerKey); - } - - return { signer, publicKey, usingLedger: true }; -}; diff --git a/cli/middleware/seed-phrase.js b/cli/middleware/seed-phrase.js deleted file mode 100644 index 985977a..0000000 --- a/cli/middleware/seed-phrase.js +++ /dev/null @@ -1,23 +0,0 @@ -const { parseSeedPhrase } = require('near-seed-phrase'); -const { utils: { KeyPair }, InMemorySigner } = require('near-api-js'); -const { InMemoryKeyStore } = require('near-api-js/lib/key_stores'); - -const implicitAccountId = require('../utils/implicit-accountid'); - -// near ... --seedPhrase="phrase" --seedPath="m/44'/397'/0'" -// near generate-key --seedPhrase="phrase" -module.exports = async function useSeedPhrase({ seedPhrase, seedPath, publicKey, accountId, networkId }, yargs) { - if (!seedPhrase) { - return; - } - if (yargs.usingLedger) { - throw new Error('Can not use both --useLedgerKey and --seedPhrase at the same time'); - } - const result = parseSeedPhrase(seedPhrase, seedPath); - publicKey = result.publicKey; - let keyStore = new InMemoryKeyStore(); - accountId = accountId || implicitAccountId(publicKey); - await keyStore.setKey(networkId, accountId, KeyPair.fromString(result.secretKey)); - let signer = new InMemorySigner(keyStore); - return { signer, publicKey, accountId }; -}; diff --git a/cli/middleware/print-options.js b/cli/print-options.js similarity index 100% rename from cli/middleware/print-options.js rename to cli/print-options.js diff --git a/cli/test/index.sh b/cli/test/index.sh deleted file mode 100755 index 0586909..0000000 --- a/cli/test/index.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash -export NODE_ENV=${NODE_ENV:-test} -OVERALL_RESULT=0 -mkdir ~/.near-config -echo '{"trackingEnabled":false}' > ~/.near-config/settings.json -for test in ./test/test_*; do - echo "" - echo "Running $test" - "$test" - if [ $? -ne 0 ]; then - echo "$test FAIL" - OVERALL_RESULT=1 - else - echo "$test SUCCESS" - fi -done - -exit $OVERALL_RESULT diff --git a/cli/test/res/fungible_token.wasm b/cli/test/res/fungible_token.wasm deleted file mode 100755 index 084be6eb6d43c743d92b844bbc57f5bfefe09be2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 169783 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 >/dev/null) -echo $ERROR -EXPECTED_ERROR=".+New account doesn't share the same top-level account.+ " -if [[ ! "$ERROR" =~ $EXPECTED_ERROR ]]; then - echo FAILURE Unexpected output creating account with different master account - exit 1 -fi - -ERROR=$(./bin/near create-account tooshortfortla --masterAccount test.far 2>&1 >/dev/null) -echo $ERROR -EXPECTED_ERROR=".+Top-level accounts must be at least.+ " -if [[ ! "$ERROR" =~ $EXPECTED_ERROR ]]; then - echo FAILURE Unexpected output when creating a short top-level account - exit 1 -fi diff --git a/cli/test/test_account_operations.sh b/cli/test/test_account_operations.sh deleted file mode 100755 index 73bebd4..0000000 --- a/cli/test/test_account_operations.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash -set -ex -rm -rf tmp-project -yarn create near-app tmp-project -cd tmp-project -timestamp=$(date +%s) -testaccount=testaccount$timestamp.test.near -echo Create account -../bin/near create-account $testaccount - -echo Get account state -RESULT=$(../bin/near state $testaccount -v | ../node_modules/.bin/strip-ansi) -echo $RESULT -EXPECTED=".+Account $testaccount.+amount:.+'100000000000000000000000000'.+ " -if [[ ! "$RESULT" =~ $EXPECTED ]]; then - echo FAILURE Unexpected output from near view - exit 1 -fi - -../bin/near delete $testaccount test.near diff --git a/cli/test/test_contract.sh b/cli/test/test_contract.sh deleted file mode 100755 index ab85d49..0000000 --- a/cli/test/test_contract.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash -set -ex -rm -rf tmp-project - -yarn create near-app tmp-project - -cd tmp-project - -timestamp=$(date +%s) -testaccount=testaccount$timestamp.test.near -../bin/near create-account $testaccount - -echo Building contract -yarn install -yarn build:contract - -echo Deploying contract -../bin/near deploy --accountId=$testaccount --wasmFile=out/main.wasm - -echo Deploying contract to temporary accountId -# TODO: Specify helperUrl in project template -yes | ../bin/near dev-deploy - -echo Calling functions -../bin/near call $testaccount setGreeting '{"message":"TEST"}' --accountId=test.near - -RESULT=$(../bin/near view $testaccount getGreeting '{"accountId":"test.near"}' --accountId=test.near -v) -TEXT=$RESULT -EXPECTED='TEST' -if [[ ! $TEXT =~ .*$EXPECTED.* ]]; then - echo FAILURE Unexpected output from near call: $RESULT - exit 1 -fi - -# base64-encoded '{"message":"BASE64ROCKS"}' -../bin/near call $testaccount setGreeting --base64 'eyJtZXNzYWdlIjoiQkFTRTY0Uk9DS1MifQ==' --accountId=test.near - -RESULT=$(../bin/near view $testaccount getGreeting '{"accountId":"test.near"}' --accountId=test.near -v) -# TODO: Refactor asserts -TEXT=$RESULT -EXPECTED='BASE64ROCKS' -if [[ ! $TEXT =~ .*$EXPECTED.* ]]; then - echo FAILURE Unexpected output from near call: $RESULT - exit 1 -fi diff --git a/cli/test/test_deploy_init_contract.sh b/cli/test/test_deploy_init_contract.sh deleted file mode 100755 index c4ccdfb..0000000 --- a/cli/test/test_deploy_init_contract.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash -set -ex -rm -rf tmp-project -# create-near-app only used to get access to test.near key file -yarn create near-app tmp-project -cd tmp-project -timestamp=$(date +%s) -testaccount=testaccount$timestamp.test.near -echo Creating account -../bin/near create-account $testaccount - -echo Deploying contract without init method -../bin/near deploy --accountId $testaccount --wasmFile ../test/res/fungible_token.wasm -ERROR=$(../bin/near view $testaccount get_balance '{"owner_id": "test.near"}' -v 2>&1 >/dev/null | ../node_modules/.bin/strip-ansi) -echo $ERROR -EXPECTED_ERROR=".+Fun token should be initialized before usage+" -if [[ ! "$ERROR" =~ $EXPECTED_ERROR ]]; then - echo FAILURE Expected message requiring initialization of contract - exit 1 -else - echo Received expected error requiring initialization -fi - -# Delete account, remake, redeploy -../bin/near delete $testaccount test.near -../bin/near create-account $testaccount -../bin/near deploy --accountId $testaccount --wasmFile ../test/res/fungible_token.wasm --initFunction new --initArgs '{"owner_id": "test.near", "total_supply": "1000000"}' -RESULT=$(../bin/near view $testaccount get_balance '{"owner_id": "test.near"}' -v | ../node_modules/.bin/strip-ansi) -echo $RESULT -if [[ $RESULT -ne 1000000 ]]; then - echo FAILURE Expected balance sent in initialization args - exit 1 -else - echo Received proper balance sent by deploy and initialization args -fi - -# Clean up by deleting account, sending back to test.near -../bin/near delete $testaccount test.near diff --git a/cli/test/test_generate_key.sh b/cli/test/test_generate_key.sh deleted file mode 100755 index 2028831..0000000 --- a/cli/test/test_generate_key.sh +++ /dev/null @@ -1,31 +0,0 @@ - -#!/bin/bash -set -ex -KEY_FILE=~/.near-credentials/$NODE_ENV/generate-key-test.json -rm -f "$KEY_FILE" -echo "Testing generating-key: new key" - -RESULT=$(./bin/near generate-key generate-key-test --networkId $NODE_ENV -v) -echo $RESULT - -if [[ ! -f "${KEY_FILE}" ]]; then - echo "FAILURE Key file doesn't exist" - exit 1 -fi - -EXPECTED=".*Key pair with ed25519:.+ public key.*" -if [[ ! "$RESULT" =~ $EXPECTED ]]; then - echo FAILURE Unexpected output from near generate-key - exit 1 -fi - -echo "Testing generating-key: key for account already exists" - -RESULT2=$(./bin/near generate-key generate-key-test --networkId $NODE_ENV -v) -echo $RESULT2 - -EXPECTED2=".*Account has existing key pair with ed25519:.+ public key.*" -if [[ ! "$RESULT2" =~ $EXPECTED2 ]]; then - echo FAILURE Unexpected output from near generate-key when key already exists - exit 1 -fi diff --git a/cli/test/unit/explorer.test.js b/cli/test/unit/explorer.test.js deleted file mode 100644 index 23dc41f..0000000 --- a/cli/test/unit/explorer.test.js +++ /dev/null @@ -1,27 +0,0 @@ - -const explorer = require('../../utils/explorer'); - -describe('generate explorer link', () => { - test('on environment with a known url', async () => { - const config = require('../../config')('development'); - const url = explorer.generateTransactionUrl('61Uc5f7L42SDWFPYHx7goMc2xEN7YN4fgtw9baHA82hY', config); - expect(url).toEqual('https://explorer.testnet.near.org/transactions/61Uc5f7L42SDWFPYHx7goMc2xEN7YN4fgtw9baHA82hY'); - }); - - test('on environment with an unknown url', async () => { - const config = require('../../config')('ci'); - const url = explorer.generateTransactionUrl('61Uc5f7L42SDWFPYHx7goMc2xEN7YN4fgtw9baHA82hY', config); - expect(url).toEqual(null); - }); - - test('unknown txn id', async () => { - const config = require('../../config')('development'); - const url = explorer.generateTransactionUrl(null, config); - expect(url).toEqual(null); - }); - - test('null options', async () => { - const url = explorer.generateTransactionUrl('61Uc5f7L42SDWFPYHx7goMc2xEN7YN4fgtw9baHA82hY', null); - expect(url).toEqual(null); - }); -}); diff --git a/cli/test/unit/inspect-response.test.js b/cli/test/unit/inspect-response.test.js deleted file mode 100644 index d4e6021..0000000 --- a/cli/test/unit/inspect-response.test.js +++ /dev/null @@ -1,20 +0,0 @@ -const inspectResponse = require('../../utils/inspect-response'); - -describe('getTxnId', () => { - test('with expected data format', async () => { - const data = { - transaction: { - hash: 'BF1iyVWTkagisho3JKKUXiPQu2sMuuLsEbvQBDYHHoKE' - } - }; - expect(inspectResponse.getTxnId(data)).toEqual('BF1iyVWTkagisho3JKKUXiPQu2sMuuLsEbvQBDYHHoKE'); - }); - - test('with null response', async () => { - expect(inspectResponse.getTxnId(null)).toEqual(null); - }); - - test('with null transaction inside response', async () => { - expect(inspectResponse.getTxnId({})).toEqual(null); - }); -}); diff --git a/cli/utils/capture-login-success.js b/cli/utils/capture-login-success.js deleted file mode 100644 index aa19e67..0000000 --- a/cli/utils/capture-login-success.js +++ /dev/null @@ -1,136 +0,0 @@ -const http = require('http'); -const url = require('url'); -const stoppable = require('stoppable'); // graceful, effective server shutdown -const tcpPortUsed = require('tcp-port-used'); // avoid port collisions - -let server; - -/** - extract arbitrary collection of fields from temporary HTTP server - server processes a single request and then shuts down gracefully - - @param fields array of fields to extract from req.url.query - @param port the port the server should use - @param hostname the hostname the server should use - */ -const payload = (fields, { port, hostname }, redirectUrl) => new Promise((resolve, reject) => { - server = stoppable(http.createServer(handler)).listen(port, hostname); - - /** - request handler for single-use node server - */ - function handler(req, res){ - try { - let parsedUrl = url.parse(req.url, true); - let results = fields.map((field) => parsedUrl.query[field]); - - if (Object.keys(parsedUrl.query).length > 0) { - res.statusCode = 200; - res.setHeader('Content-Type', 'text/html'); - // TODO: Make code more specialized (vs handling generic fields) and only output this if login succeeded end to end - res.end(renderWebPage('You are logged in. Please close this window.'), () => { - server.stop(); - resolve(results); - }); - } else { - res.writeHead(302, { Location: redirectUrl }); - res.end(); - } - } catch (e) { - console.error('Unexpected error: ', e); - res.statusCode = 400; - res.end('It\'s a scam!'); - server.stop(); - reject(new Error('Failed to capture accountId')); - } - } -}); - - -/** - attempt to find the first suitable (open) port - @param port the starting port on the computer to scan for availability - @param hostname the hostname of the machine on which to scan for open ports - @param range the number of ports to try scanning before giving up - */ -const callback = async (port = 3000, hostname = '127.0.0.1', range = 10) => { - if (process.env.GITPOD_WORKSPACE_URL) { - // NOTE: Port search interferes with GitPod port management - return { port, hostname }; - } - - const start = port; - const end = start + range; - let inUse = true; - - for (;port <= end; port++) { - try { - inUse = await tcpPortUsed.check(port, hostname); - if (!inUse) { - break; // unused port found - } - } catch (e) { - console.error('Error while scanning for available ports.', e.message); - } - } - - if(inUse) { - throw new Error(`All ports in use: [ ${start} - ${end} ]`); - } - - return { port, hostname }; -}; - -const cancel = () => { - if (server) server.stop(); -}; - -module.exports = { payload, callback, cancel }; - - -/** - helper to render a proper success page - */ -function renderWebPage(message){ - const title = 'NEAR Account Authorization Success'; - - // logo and font from https://near.org/brand/ - return ` - - - - - - - ${title} - - - - -
-
- near_logo -

${message}

-
-
- - - `; -} diff --git a/cli/utils/connect.js b/cli/utils/connect.js deleted file mode 100644 index 1052333..0000000 --- a/cli/utils/connect.js +++ /dev/null @@ -1,6 +0,0 @@ -const { connect: nearConnect } = require('near-api-js'); - -module.exports = async function connect({ keyStore, ...options }) { - // TODO: Avoid need to wrap in deps - return await nearConnect({ ...options, deps: { keyStore }}); -}; diff --git a/cli/utils/exit-on-error.js b/cli/utils/exit-on-error.js deleted file mode 100644 index 019cc00..0000000 --- a/cli/utils/exit-on-error.js +++ /dev/null @@ -1,42 +0,0 @@ -const eventtracking = require('./eventtracking'); -const inspectResponse = require('./inspect-response'); - -// This is a workaround to get Mixpanel to log a crash -process.on('exit', () => { - const crashEventProperties = { - near_cli_command: process.env.NEAR_CLI_ERROR_LAST_COMMAND, - error_message: process.env.NEAR_CLI_LAST_ERROR - }; - require('child_process').fork(__dirname + '/log-event.js', ['node'], { - silent: true, - detached: true, - env: { - NEAR_CLI_EVENT_ID: eventtracking.EVENT_ID_ERROR, - NEAR_CLI_EVENT_DATA: JSON.stringify(crashEventProperties) - } - }); -}); - -module.exports = (promiseFn) => async (...args) => { - const command = args[0]['_']; - process.env.NEAR_CLI_ERROR_LAST_COMMAND = command; - process.env.NEAR_CLI_NETWORK_ID = require('../get-config')()['networkId']; - const options = args[0]; - const eventId = `event_id_shell_${command}_start`; - require('child_process').fork(__dirname + '/log-event.js', ['node'], { - silent: true, - detached: true, - env: { - NEAR_CLI_EVENT_ID: eventId, - NEAR_CLI_EVENT_DATA: JSON.stringify({}) - } - }); - const promise = promiseFn.apply(null, args); - try { - await promise; - } catch (e) { - process.env.NEAR_CLI_LAST_ERROR = e.message; - inspectResponse.prettyPrintError(e, options); - process.exit(1); - } -}; \ No newline at end of file diff --git a/cli/utils/explorer.js b/cli/utils/explorer.js deleted file mode 100644 index bb71fc8..0000000 --- a/cli/utils/explorer.js +++ /dev/null @@ -1,22 +0,0 @@ -// Handle functionality related to explorer - -const generateTransactionUrl = (txnId, options) => { - if (!txnId || !options) { - return null; - } - const explorerUrl = options.explorerUrl; - return explorerUrl ? `${explorerUrl}/transactions/${txnId}` : null; -}; - -const printTransactionUrl = (txnId, options) => { - const txnUrl = generateTransactionUrl(txnId, options); - if (txnUrl) { - console.log('To see the transaction in the transaction explorer, please open this url in your browser'); - console.log(txnUrl); - } -}; - -module.exports = { - generateTransactionUrl, - printTransactionUrl, -}; \ No newline at end of file diff --git a/cli/utils/implicit-accountid.js b/cli/utils/implicit-accountid.js deleted file mode 100644 index 304607d..0000000 --- a/cli/utils/implicit-accountid.js +++ /dev/null @@ -1,5 +0,0 @@ -const { decode } = require('bs58'); - -module.exports = (publicKey) => { - return decode(publicKey.replace('ed25519:', '')).toString('hex'); -}; diff --git a/cli/utils/inspect-response.js b/cli/utils/inspect-response.js deleted file mode 100644 index 942b46c..0000000 --- a/cli/utils/inspect-response.js +++ /dev/null @@ -1,96 +0,0 @@ -const explorer = require('./explorer'); -const config = require('../get-config')(); -const chalk = require('chalk'); // colorize output -const util = require('util'); - - -const checkForAccDoesNotExist = (error, options) => { - if(!String(error).includes('does not exist while viewing')) return false; - - const suffixesToNetworks = {near:'mainnet', testnet:'testnet', betanet:'betanet'}; - - const currentNetwork = config.helperAccount; - console.log(chalk`\n{bold.red Account {bold.white ${options.accountId}} is not found in {bold.white ${suffixesToNetworks[currentNetwork]}}\n}`); - - const accSuffix = String(options.accountId).match('[^.]*$')[0]; - const accNetwork = suffixesToNetworks[accSuffix]; - if (currentNetwork != accSuffix && accNetwork) { - console.log(chalk`{bold.white Use export NEAR_ENV=${accNetwork} to use ${accNetwork} accounts. \n}`); - } - - return true; -}; - -const prettyPrintResponse = (response, options) => { - if (options.verbose) { - console.log(formatResponse(response)); - } - const txnId = getTxnId(response); - if (txnId) { - console.log(`Transaction Id ${txnId}`); - explorer.printTransactionUrl(txnId, options); - } -}; - -const prettyPrintError = (error, options) => { - if (checkForAccDoesNotExist(error, options)) return; - - console.error('An error occured'); - console.error(error.stack); - console.error(formatResponse(error)); - const txnId = getTxnIdFromError(error); - if (txnId) { - console.log(`We attempted to send transaction ${txnId} to NEAR, but something went wrong.`); - explorer.printTransactionUrl(txnId, options); - console.log('Note: if the transaction was invalid (e.g. not enough balance), it will show as "Not started" or "Finalizing"'); - } -}; - -const formatResponse = (response) => { - return util.inspect(response, { - // showHidden: true, - depth: null, - colors: Boolean(process.stdout.isTTY && process.stdout.hasColors()), - maxArrayLength: null - }); -}; - -const getTxnIdFromError = (error) => { - // Currently supported error format: - // { - // [stack]: 'Error: Sender jane.betanet does not have enough balance 45000000521675913419670000 for operation costing 1000000000002265303009375000\n' + - // ... - // [message]: 'Sender jane.betanet does not have enough balance 45000000521675913419670000 for operation costing 1000000000002265303009375000', - // type: 'NotEnoughBalance', - // context: ErrorContext { - // transactionHash: 'FyavUCyvZ5G1JLTdnXSZd3VoaFEaGRXnmDFwhmNeaVC6' - // }, - // balance: '45000000521675913419670000', - // cost: '1000000000002265303009375000', - // signer_id: 'jane.betanet' - // } - - if (!error || !error.context) return null; - return error.context.transactionHash; -}; - -const getTxnId = (response) => { - // Currently supported response format: - //{ - // ... - // transaction: { - // ... - // hash: 'BF1iyVWTkagisho3JKKUXiPQu2sMuuLsEbvQBDYHHoKE' - // }, - if (!response || !response.transaction) { - return null; - } - return response.transaction.hash; -}; - -module.exports = { - prettyPrintResponse, - prettyPrintError, - formatResponse, - getTxnId, -}; diff --git a/cli/utils/readline.js b/cli/utils/readline.js deleted file mode 100644 index 606df85..0000000 --- a/cli/utils/readline.js +++ /dev/null @@ -1,37 +0,0 @@ -const readline = require('readline'); - -const askYesNoQuestion = async (question, defaultResponse = false) => { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - try { - for (let attempts = 0; attempts < 10; attempts++) { - const answer = await new Promise((resolve) => { - rl.question( - question, - async (response) => { - if (response.toLowerCase() == 'y') { - resolve(true); - } else if ( - response.toLowerCase() == 'n' - ) { - resolve(false); - } - resolve(undefined); - } - ); - }); - if (answer !== undefined) { - return answer; - } - } - - // Use default response when no valid response is obtained - return defaultResponse; - } finally { - rl.close(); - } -}; - -module.exports = { askYesNoQuestion }; \ No newline at end of file diff --git a/cli/utils/settings.js b/cli/utils/settings.js deleted file mode 100644 index 4fe16b3..0000000 --- a/cli/utils/settings.js +++ /dev/null @@ -1,45 +0,0 @@ -const fs = require('fs'); -const homedir = require('os').homedir(); -const path = require('path'); - - -// Persistent shell settings -const SETTINGS_FILE_NAME = 'settings.json'; -const SETTINGS_DIR = '.near-config'; - -const getShellSettings = () => { - const nearPath = path.join(homedir, SETTINGS_DIR); - try { - if (!fs.existsSync(nearPath)) { - fs.mkdirSync(nearPath); - } - const shellSettingsPath = path.join(nearPath, SETTINGS_FILE_NAME); - if (!fs.existsSync(shellSettingsPath)) { - return {}; - } else { - return JSON.parse(fs.readFileSync(shellSettingsPath, 'utf8')); - } - } catch (e) { - console.log(e); - } - return {}; -}; - -const saveShellSettings = (settings) => { - const nearPath = path.join(homedir, SETTINGS_DIR); - try { - if (!fs.existsSync(nearPath)) { - fs.mkdirSync(nearPath); - } - const shellSettingsPath = path.join(nearPath, SETTINGS_FILE_NAME); - const indentationSize = 4; - fs.writeFileSync(shellSettingsPath, JSON.stringify(settings, null, indentationSize)); - } catch (e) { - console.log(e); - } -}; - -module.exports = { - getShellSettings, - saveShellSettings -}; \ No newline at end of file diff --git a/cli/utils/validators-info.js b/cli/utils/validators-info.js deleted file mode 100644 index 499fa4f..0000000 --- a/cli/utils/validators-info.js +++ /dev/null @@ -1,96 +0,0 @@ -const { validators, utils } = require('near-api-js'); -const BN = require('bn.js'); -const AsciiTable = require('ascii-table'); - -async function validatorsInfo(near, epochId) { - const genesisConfig = await near.connection.provider.sendJsonRpc('EXPERIMENTAL_genesis_config', {}); - const result = await near.connection.provider.sendJsonRpc('validators', [epochId]); - result.genesisConfig = genesisConfig; - result.numSeats = genesisConfig.num_block_producer_seats + genesisConfig.avg_hidden_validator_seats_per_shard.reduce((a, b) => a + b); - return result; -} - -async function showValidatorsTable(near, epochId) { - const result = await validatorsInfo(near, epochId); - const seatPrice = validators.findSeatPrice(result.current_validators, result.numSeats); - result.current_validators = result.current_validators.sort((a, b) => -new BN(a.stake).cmp(new BN(b.stake))); - var validatorsTable = new AsciiTable(); - validatorsTable.setHeading('Validator Id', 'Stake', '# Seats', '% Online', 'Blocks produced', 'Blocks expected'); - console.log(`Validators (total: ${result.current_validators.length}, seat price: ${utils.format.formatNearAmount(seatPrice, 0)}):`); - result.current_validators.forEach((validator) => { - validatorsTable.addRow( - validator.account_id, - utils.format.formatNearAmount(validator.stake, 0), - new BN(validator.stake).div(seatPrice), - `${Math.floor(validator.num_produced_blocks / validator.num_expected_blocks * 10000) / 100}%`, - validator.num_produced_blocks, - validator.num_expected_blocks); - }); - console.log(validatorsTable.toString()); -} - -async function showNextValidatorsTable(near) { - const result = await validatorsInfo(near, null); - const nextSeatPrice = validators.findSeatPrice(result.next_validators, result.numSeats); - result.next_validators = result.next_validators.sort((a, b) => -new BN(a.stake).cmp(new BN(b.stake))); - const diff = validators.diffEpochValidators(result.current_validators, result.next_validators); - console.log(`\nNext validators (total: ${result.next_validators.length}, seat price: ${utils.format.formatNearAmount(nextSeatPrice, 0)}):`); - let nextValidatorsTable = new AsciiTable(); - nextValidatorsTable.setHeading('Status', 'Validator', 'Stake', '# Seats'); - diff.newValidators.forEach((validator) => nextValidatorsTable.addRow( - 'New', - validator.account_id, - utils.format.formatNearAmount(validator.stake, 0), - new BN(validator.stake).div(nextSeatPrice))); - diff.changedValidators.forEach((changeValidator) => nextValidatorsTable.addRow( - 'Rewarded', - changeValidator.next.account_id, - `${utils.format.formatNearAmount(changeValidator.current.stake, 0)} -> ${utils.format.formatNearAmount(changeValidator.next.stake, 0)}`, - new BN(changeValidator.next.stake).div(nextSeatPrice))); - diff.removedValidators.forEach((validator) => nextValidatorsTable.addRow('Kicked out', validator.account_id, '-', '-')); - console.log(nextValidatorsTable.toString()); -} - -function combineValidatorsAndProposals(validators, proposalsMap) { - // TODO: filter out all kicked out validators. - let result = validators.filter((validator) => !proposalsMap.has(validator.account_id)); - return result.concat([...proposalsMap.values()]); -} - -async function showProposalsTable(near) { - const result = await validatorsInfo(near, null); - let currentValidators = new Map(); - result.current_validators.forEach((v) => currentValidators.set(v.account_id, v)); - let proposals = new Map(); - result.current_proposals.forEach((p) => proposals.set(p.account_id, p)); - const combinedProposals = combineValidatorsAndProposals(result.current_validators, proposals); - const expectedSeatPrice = validators.findSeatPrice(combinedProposals, result.numSeats); - const combinedPassingProposals = combinedProposals.filter((p) => new BN(p.stake).gte(expectedSeatPrice)); - console.log(`Proposals for the epoch after next (new: ${proposals.size}, passing: ${combinedPassingProposals.length}, expected seat price = ${utils.format.formatNearAmount(expectedSeatPrice, 0)})`); - const proposalsTable = new AsciiTable(); - proposalsTable.setHeading('Status', 'Validator', 'Stake => New Stake', '# Seats'); - combinedProposals.sort((a, b) => -new BN(a.stake).cmp(new BN(b.stake))).forEach((proposal) => { - let kind = ''; - if (new BN(proposal.stake).gte(expectedSeatPrice)) { - kind = proposals.has(proposal.account_id) ? 'Proposal(Accepted)' : 'Rollover'; - } else { - kind = proposals.has(proposal.account_id) ? 'Proposal(Declined)' : 'Kicked out'; - } - let stake_fmt = utils.format.formatNearAmount(proposal.stake, 0); - if (currentValidators.has(proposal.account_id) && proposals.has(proposal.account_id)) { - stake_fmt = `${utils.format.formatNearAmount(currentValidators.get(proposal.account_id).stake, 0)} => ${stake_fmt}`; - } - proposalsTable.addRow( - kind, - proposal.account_id, - stake_fmt, - new BN(proposal.stake).div(expectedSeatPrice) - ); - }); - console.log(proposalsTable.toString()); - console.log('Expected seat price is calculated based on observed so far proposals and validators.'); - console.log('It can change from new proposals or some validators going offline.'); - console.log('Note: this currently doesn\'t account for offline kickouts and rewards for current epoch'); -} - -module.exports = { showValidatorsTable, showNextValidatorsTable, showProposalsTable }; \ No newline at end of file diff --git a/cli/utils/verify-account.js b/cli/utils/verify-account.js deleted file mode 100644 index 307b291..0000000 --- a/cli/utils/verify-account.js +++ /dev/null @@ -1,41 +0,0 @@ -// npm imports -const chalk = require('chalk'); - -// local imports -const connect = require('./connect'); - -module.exports = async (accountId, keyPair, options) => { - try { - // check that the key got added - const near = await connect(options); - let account = await near.account(accountId); - let keys = await account.getAccessKeys(); - let publicKey = keyPair.getPublicKey().toString(); - const short = key => `${key.substring(0, 14)}...`; // keep the public key readable - - let keyFound = keys.some( - key => key.public_key == keyPair.getPublicKey().toString() - ); - if (keyFound) { - const keyStore = near.config.deps.keyStore; - await keyStore.setKey(options.networkId, accountId, keyPair); - console.log(chalk`Logged in as [ {bold ${accountId}} ] with public key [ {bold ${short(publicKey)}} ] successfully\n` - ); - return true; - } else { - console.log(chalk`The account you provided {bold.red [ {bold.white ${accountId}} ] has not authorized the expected key [ {bold.white ${short(publicKey)}} ]} Please try again.\n` - ); - return false; - } - } catch (e) { - if (/Account ID/.test(e.message)) { - console.log(chalk`\n{bold.red You need to provide a valid account ID to login}. Please try logging in again.\n`); - return false; - } else if (/does not exist/.test(e.message)) { - console.log(chalk`\nThe account you provided {bold.red [ {bold.white ${accountId}} ] does not exist on the [ {bold.white ${options.networkId}} ] network} (using ${options.nodeUrl})\n`); - return false; - } else { - throw e; - } - } -}; diff --git a/src/actions.js b/src/actions.js new file mode 100644 index 0000000..b3cdf76 --- /dev/null +++ b/src/actions.js @@ -0,0 +1,121 @@ +require('dotenv').config() +const contractAbi = require('../src/contract_abi.json') +import { utils } from 'near-api-js' +import Big from 'big.js' +import NearProvider from './near' +import chalk from 'chalk' + +const log = console.log +export const env = process.env.NODE_ENV || 'development' +export const WAIT_INTERVAL_MS = process.env.WAIT_INTERVAL_MS || 500 +export const AGENT_ACCOUNT_ID = process.env.AGENT_ACCOUNT_ID || 'crond-agent' +export const BASE_GAS_FEE = 300000000000000 +export const BASE_ATTACHED_PAYMENT = 0 + +export const Near = new NearProvider({ + networkId: env === 'production' ? 'mainnet' : 'testnet', + accountId: AGENT_ACCOUNT_ID, +}) +let cronManager = null +let agentAccount = null + +export async function connect() { + await Near.getNearConnection() +} + +export async function getCronManager() { + if (cronManager) return cronManager + const abi = contractAbi.abis.manager + const contractId = contractAbi[env].manager + cronManager = await Near.getContractInstance(contractId, abi) + return cronManager +} + +export async function registerAgent() { + const manager = await getCronManager() + + // NOTE: Optional "payable_account_id" here + try { + await manager.register_agent({}, BASE_GAS_FEE, BASE_ATTACHED_PAYMENT) + log(`Registered Agent: ${chalk.white(AGENT_ACCOUNT_ID)}`) + } catch (e) { + log(`${chalk.red('Registered Failed: ')}${chalk.bold.red('Please remove your credentials and trying again.')}`) + process.exit(1) + } +} + +export async function getAgent() { + const manager = await getCronManager() + return manager.get_agent({ pk: agentAccount }) +} + +export async function checkAgentBalance() { + const balance = await Near.getAccountBalance() + const formattedBalance = utils.format.formatNearAmount(balance) + const hasEnough = Big(balance).gt(BASE_GAS_FEE) + log(` + Agent Account: ${chalk.white(AGENT_ACCOUNT_ID)} + Agent Balance: ${!hasEnough ? chalk.red(formattedBalance) : chalk.green(formattedBalance)} + `) + if (!hasEnough) { + log(` + ${chalk.red('Your agent account does not have enough to pay for signing transactions.')} + Use the following steps: + ${chalk.bold.white('1. Copy your account id: ')}${chalk.underline.white(AGENT_ACCOUNT_ID)} + ${chalk.bold.white('2. Use the web wallet to send funds: ')}${chalk.underline.blue(Near.config.walletUrl + '/send-money')} + ${chalk.bold.white('3. Use NEAR CLI to send funds: ')} "near send OTHER_ACCOUNT ${AGENT_ACCOUNT_ID} ${(Big(BASE_GAS_FEE).mul(4))}" + `) + process.exit(1) + } +} + +export async function runAgentTick() { + const manager = await getCronManager() + let tasks = [] + + // 1. Check for tasks + tasks = await manager.get_tasks() + log(`${chalk.gray(new Date().toISOString())} Current Tasks: ${chalk.blueBright(tasks.length)}`) + + // 2. Sign task and submit to chain + if (tasks && tasks.length > 0) { + try { + const res = await manager.proxy_call({}, BASE_GAS_FEE, BASE_ATTACHED_PAYMENT) + console.log('runAgentTick res', res); + } catch (e) { + console.log(e) + } + } + + // Wait, then loop again. + setTimeout(runAgentTick, WAIT_INTERVAL_MS) +} + +export async function agentFunction(method, args) { + const manager = await getCronManager() + try { + const res = await manager[method](args, BASE_GAS_FEE, BASE_ATTACHED_PAYMENT) + console.log('agentFunction res', method, res); + } catch (e) { + console.log(e) + } +} + +export async function bootstrapAgent() { + await connect() + + // 1. Check for local signing keys, if none - generate new and halt until funded + agentAccount = await Near.getAccountCredentials(AGENT_ACCOUNT_ID) + + // 2. Check for balance, if enough to execute txns, start main tasks + await checkAgentBalance() + + // 3. Check if agent is registered, if not register immediately before proceeding + try { + await getAgent() + log(`Verified Agent: ${chalk.white(AGENT_ACCOUNT_ID)}`) + } catch (e) { + log(`No Agent: ${chalk.gray('trying to register...')}`) + await registerAgent() + } +} \ No newline at end of file diff --git a/src/index.js b/src/index.js index 335eaf6..b9edab1 100644 --- a/src/index.js +++ b/src/index.js @@ -1,108 +1,9 @@ -require('dotenv').config() -const contractAbi = require('../src/contract_abi.json') -import { utils } from 'near-api-js' -import Big from 'big.js' -import NearProvider from './near' -import chalk from 'chalk' - -const log = console.log -const env = process.env.NODE_ENV || 'development' -const WAIT_INTERVAL_MS = process.env.WAIT_INTERVAL_MS || 500 -const AGENT_ACCOUNT_ID = process.env.AGENT_ACCOUNT_ID || 'crond-agent' -const BASE_GAS_FEE = 300000000000000 -const BASE_ATTACHED_PAYMENT = 0 - -const Near = new NearProvider({ - networkId: env === 'production' ? 'mainnet' : 'testnet', - accountId: AGENT_ACCOUNT_ID, -}) -let cronManager = null -let agentAccount = null - -async function getCronManager() { - if (cronManager) return cronManager - const abi = contractAbi.abis.manager - const contractId = contractAbi[env].manager - cronManager = await Near.getContractInstance(contractId, abi) - return cronManager -} - -async function registerAgent() { - const manager = await getCronManager() - - // NOTE: Optional "payable_account_id" here - try { - await manager.register_agent({}, BASE_GAS_FEE, BASE_ATTACHED_PAYMENT) - log(`Registered Agent: ${chalk.white(AGENT_ACCOUNT_ID)}`) - } catch (e) { - log(`${chalk.red('Registered Failed: ')}${chalk.bold.red('Please remove your credentials and trying again.')}`) - process.exit(1) - } -} - -async function getAgent() { - const manager = await getCronManager() - return manager.get_agent({ pk: agentAccount }) -} - -async function runAgentTick() { - const manager = await getCronManager() - let tasks = [] - - // 1. Check for tasks - tasks = await manager.get_tasks() - log(`${chalk.gray(new Date().toISOString())} Current Tasks: ${chalk.blueBright(tasks.length)}`) - - // 2. Sign task and submit to chain - if (tasks && tasks.length > 0) { - try { - const res = await manager.proxy_call({}, BASE_GAS_FEE, BASE_ATTACHED_PAYMENT) - console.log('runAgentTick res', res); - } catch (e) { - console.log(e) - } - } - - // Wait, then loop again. - setTimeout(runAgentTick, WAIT_INTERVAL_MS) -} +import * as actions from './actions' // Cron Agent Task Loop (async () => { - await Near.getNearConnection() - - // 1. Check for local signing keys, if none - generate new and halt until funded - agentAccount = await Near.getAccountCredentials(AGENT_ACCOUNT_ID) - - // 2. Check for balance, if enough to execute txns, start main tasks - const balance = await Near.getAccountBalance() - const formattedBalance = utils.format.formatNearAmount(balance) - const hasEnough = Big(balance).gt(BASE_GAS_FEE) - log(` - Agent Account: ${chalk.white(AGENT_ACCOUNT_ID)} - Agent Balance: ${!hasEnough ? chalk.red(formattedBalance) : chalk.green(formattedBalance)} - `) - if (!hasEnough) { - log(` - ${chalk.red('Your agent account does not have enough to pay for signing transactions.')} - Use the following steps: - ${chalk.bold.white('1. Copy your account id: ')}${chalk.underline.white(AGENT_ACCOUNT_ID)} - ${chalk.bold.white('2. Use the web wallet to send funds: ')}${chalk.underline.blue(Near.config.walletUrl + '/send-money')} - ${chalk.bold.white('3. Use NEAR CLI to send funds: ')} "near send OTHER_ACCOUNT ${AGENT_ACCOUNT_ID} ${(Big(BASE_GAS_FEE).mul(4))}" - `) - process.exit(1) - return - } - - // 3. Check if agent is registered, if not register immediately before proceeding - try { - await getAgent() - log(`Verified Agent: ${chalk.white(AGENT_ACCOUNT_ID)}`) - } catch (e) { - log(`No Agent: ${chalk.gray('trying to register...')}`) - await registerAgent() - } + await actions.bootstrapAgent() // MAIN AGENT LOOP - runAgentTick() + actions.runAgentTick() })() \ No newline at end of file