croncat
Version:
cron.cat CLI and Agent Runner
226 lines (200 loc) • 8.22 kB
JavaScript
import * as config from './configuration'
import * as util from './util'
import { utils } from 'near-api-js'
import Big from 'big.js'
import chalk from 'chalk'
let croncatSettings = null
let agentSettings = {}
let agentAccount = null
export async function getAgentBalance() {
try {
const balance = await util.Near.getAccountBalance()
return balance
} catch (e) {
console.log(`${chalk.red('NEAR RPC Failed')}`)
await util.notifySlack(`*Attention!* NEAR ${near_env} RPC Failed to retrieve balance!`)
process.exit(1)
}
}
// NOTE: Optional "payable_account_id" here
export async function registerAgent(agentId, payable_account_id) {
const account = agentId || config.AGENT_ACCOUNT_ID
const manager = await util.getCronManager(account)
try {
const res = await manager.register_agent({
args: { payable_account_id: payable_account_id || account },
gas: config.BASE_GAS_FEE,
amount: config.BASE_REGISTER_AGENT_FEE,
})
console.log(`Registered Agent: ${chalk.blue(account)}`)
util.dbug('REGISTER ARGS', res);
} catch (e) {
util.dbug(e);
if(e.type === 'KeyNotFound') {
console.log(`${chalk.red('Agent Registration Failed:')} ${chalk.bold.red(`Please login to your account '${account}' and try again.`)}`)
} else {
console.log(`${chalk.red('Agent Registration Failed:')} ${chalk.bold.red('Please remove your credentials and try again.')}`)
}
process.exit(1)
}
}
export async function getAgent(agentId = config.AGENT_ACCOUNT_ID) {
const manager = await util.getCronManager()
try {
const res = await manager.get_agent({ account_id: agentId })
return res
} catch (ge) {
util.dbug(ge);
}
}
export async function checkAgentBalance() {
const balance = await getAgentBalance()
const formattedBalance = utils.format.formatNearAmount(balance)
const hasEnough = Big(balance).gt(config.BASE_GAS_FEE)
console.log(`Agent Account: ${chalk.white(config.AGENT_ACCOUNT_ID)}
Agent Balance: ${!hasEnough ? chalk.red(formattedBalance) : chalk.green(formattedBalance)}`)
if (!hasEnough) {
console.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(config.AGENT_ACCOUNT_ID)}
${chalk.bold.white('2. Use the web wallet to send funds: ')}${chalk.underline.blue(util.Near.config.walletUrl + '/send-money')}
${chalk.bold.white('3. Use NEAR CLI to send funds: ')} "near send OTHER_ACCOUNT ${config.AGENT_ACCOUNT_ID} ${(Big(config.BASE_GAS_FEE).mul(4))}"
`)
process.exit(0)
}
}
export async function checkAgentTaskBalance() {
const balance = await getAgentBalance()
const notEnough = Big(balance).lt(config.AGENT_MIN_TASK_BALANCE)
if (notEnough) {
console.log(`
${chalk.red('Agent is running low on funds, attempting to refill from rewards...')}
`)
await refillAgentTaskBalance()
}
}
export async function refillAgentTaskBalance() {
try {
const manager = await util.getCronManager()
await manager.withdraw_task_balance({ args: {}, gas: config.BASE_GAS_FEE })
const balance = await getAgentBalance()
const notEnough = Big(balance).lt(config.AGENT_MIN_TASK_BALANCE)
if (notEnough) {
console.log(`${chalk.red('Balance too low.')}`)
await notifySlack(`*Attention!* Not enough balance to execute tasks, refill please.`)
process.exit(1)
} else {
console.log(`Agent Refilled, Balance: ${chalk.blue(utils.format.formatNearAmount(balance))}`)
await notifySlack(`Agent Refilled, Balance: *${utils.format.formatNearAmount(balance)}*`)
}
} catch (e) {
console.log(`${chalk.red('No balance to withdraw.')}`)
await notifySlack(`*Attention!* No balance to withdraw.`)
process.exit(1)
}
}
let agentBalanceCheckIdx = 0
export const pingAgentBalance = async () => {
// Logic will trigger on initial run, then every 5th txn
// NOTE: This is really only useful if the payout account is the same as the agent
if (config.AGENT_AUTO_REFILL && agentBalanceCheckIdx === 0) {
await checkAgentTaskBalance()
}
agentBalanceCheckIdx++
if (agentBalanceCheckIdx > 5) agentBalanceCheckIdx = 0
}
// Checks if need to re-register agent based on tasks getting missed
export const reRegisterAgent = async () => {
if (!config.AGENT_AUTO_RE_REGISTER) process.exit(1)
await registerAgent()
}
export const currentStatus = () => {
return agentSettings.status || 'Pending'
}
export const settings = () => {
return agentSettings || {}
}
// returns if agent is active or not
export const checkStatus = async () => {
let isActive = false
let previousAgentSettings = { ...agentSettings }
try {
agentSettings = await getAgent()
} catch (ae) {
util.dbug(ae)
agentSettings = {}
// if no status, trigger a delayed retry
return isActive
}
// Check agent is active & able to run tasks
if (!agentSettings || !agentSettings.status || agentSettings.status !== 'Active') {
console.log(`Agent Status: ${chalk.white(agentSettings.status)}`)
}
// Alert if agent changes status:
if (previousAgentSettings.status !== agentSettings.status) {
console.log(`Agent Status: ${chalk.white(agentSettings.status)}`)
await util.notifySlack(`*Agent Status Update:*\nYour agent is now a status of *${agentSettings.status}*`)
// At this point we could check if we need to re-register the agent if enough remaining balance, and status went from active to pending or none.
if (!agentSettings.status) return reRegisterAgent()
}
// Use agentSettings to check if the maximum missed slots have happened, stop and notify!
let last_missed_slot = agentSettings.last_missed_slot;
if (last_missed_slot !== 0) {
if (last_missed_slot > (parseInt(taskRes[1]) + (croncatSettings.agents_eject_threshold * croncatSettings.slot_granularity))) {
const ejectMsg = 'Agent has been ejected! Too many slots missed!'
console.log(`${chalk.red(ejectMsg)}`)
await util.notifySlack(`*${ejectMsg}*`)
// Assess if re-register
return reRegisterAgent()
}
}
return true
}
// Is this all i need to do? kinda seemed too easy... ROFL
export async function run() {
await checkStatus()
await pingAgentBalance()
// Wait, then loop again.
setTimeout(run, config.WAIT_INTERVAL_MS)
}
// Initialize the agent & all configs, returns TRUE if agent is active
export async function bootstrap() {
await util.connect()
const agentId = config.AGENT_ACCOUNT_ID
// 1. Check for local signing keys, if none - generate new and halt until funded
agentAccount = `${await util.Near.getAccountCredentials(agentId)}`
// 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
let requiresRegister = false
try {
agentSettings = await getAgent(agentId)
if (!agentSettings) {
if (config.AGENT_AUTO_RE_REGISTER) {
requiresRegister = true
} else {
console.log(`No Agent: ${chalk.red('Please register')}`)
process.exit(0);
}
} else {
console.log(`${chalk.gray('Registered Agent: ')}${chalk.white(agentId)}`)
}
croncatSettings = await util.getCroncatInfo()
if (!croncatSettings) {
console.log(`No Croncat Deployed At this Network`)
process.exit(1);
}
} catch (e) {
util.dbug(e);
if (config.AGENT_AUTO_RE_REGISTER) requiresRegister = true
else console.log(`No Agent: ${chalk.red('Please register')}`)
}
if (requiresRegister === true) {
console.log(`No Agent: ${chalk.gray('Attempting to register...')}`)
await registerAgent(agentId)
}
console.log(`${chalk.gray('Agent Status: ')}${chalk.white(agentSettings.status)}`)
if (agentSettings.status === 'Pending') console.log(`${chalk.yellow('Agent waiting until croncat manager changes agent status to Active...')}\n${chalk.gray('Do not stop this process unless you are done being a croncat agent, see https://cron.cat/tasks for more info')}`)
return agentSettings && agentSettings.status === 'Active' ? true : false
}