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 084be6e..0000000 Binary files a/cli/test/res/fungible_token.wasm and /dev/null differ diff --git a/cli/test/test_account_creation.sh b/cli/test/test_account_creation.sh deleted file mode 100755 index e598d03..0000000 --- a/cli/test/test_account_creation.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -set -x - -timestamp=$(date +%s) -testaccount=testaccount$timestamp.test.near - -ERROR=$(./bin/near create-account $testaccount --masterAccount test.far 2>&1 >/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