<?php

class ApiController extends CommonController
{
	public $defaultAction='status';

	/////////////////////////////////////////////////

	private function apiGuard($bucket, $limit)
	{
		$client_ip = arraySafeVal($_SERVER,'REMOTE_ADDR');
		$whitelisted = isAdminIP($client_ip);
		if (!$whitelisted && is_file(VEXON_LOGS.'/overloaded')) {
			header('HTTP/1.0 503 Disabled, server overloaded');
			return false;
		}
		if(!$whitelisted && !LimitRequest($bucket, $limit)) {
			return false;
		}

		return true;
	}

	private function jsonOut($data, $cache_key=null, $ttl=0)
	{
		$json = json_encode($data);
		header('Content-Type: application/json');
		header('Access-Control-Allow-Origin: *');
		header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
		header('Access-Control-Allow-Headers: Content-Type, X-Vexon-Wallet');
		echo $json;
		if ($cache_key && $ttl > 0)
			controller()->memcache->set($cache_key, $json, $ttl, MEMCACHE_COMPRESSED);
	}

	private function loadExplorerUtil()
	{
		$path = Yii::getPathOfAlias('application.modules.explorer').'/util.php';
		if (is_file($path))
			require_once($path);
	}

	private function vexonBlockTime($coin)
	{
		$block_time = $coin && !empty($coin->block_time) ? intval($coin->block_time) : 0;
		if ($block_time <= 0)
			$block_time = defined('VEXON_BLOCK_TIME_ESTIMATE') ? intval(VEXON_BLOCK_TIME_ESTIMATE) : 150;
		return max(1, $block_time);
	}

	private function summarizeBlock($block)
	{
		if (!is_array($block))
			return array();
		$tx = arraySafeVal($block, 'tx', array());
		if (!is_array($tx))
			$tx = array();

		return array(
			'hash'=>arraySafeVal($block, 'hash', ''),
			'height'=>intval(arraySafeVal($block, 'height', 0)),
			'confirmations'=>intval(arraySafeVal($block, 'confirmations', 0)),
			'time'=>intval(arraySafeVal($block, 'time', 0)),
			'size'=>intval(arraySafeVal($block, 'size', 0)),
			'difficulty'=>doubleval(arraySafeVal($block, 'difficulty', 0)),
			'bits'=>arraySafeVal($block, 'bits', ''),
			'merkleroot'=>arraySafeVal($block, 'merkleroot', ''),
			'previousblockhash'=>arraySafeVal($block, 'previousblockhash', ''),
			'nextblockhash'=>arraySafeVal($block, 'nextblockhash', ''),
			'tx_count'=>count($tx),
			'tx'=>$tx,
		);
	}

	private function summarizeTransaction($tx)
	{
		if (!is_array($tx))
			return array();

		$vout = arraySafeVal($tx, 'vout', array());
		if (!is_array($vout))
			$vout = array();

		$total = 0.0;
		$outputs = array();
		foreach($vout as $out) {
			$value = (double) arraySafeVal($out, 'value', 0);
			$total += $value;
			$script = arraySafeVal($out, 'scriptPubKey', array());
			$addresses = arraySafeVal($script, 'addresses', array());
			if (!is_array($addresses))
				$addresses = array();
			$outputs[] = array(
				'n'=>intval(arraySafeVal($out, 'n', 0)),
				'value'=>$value,
				'type'=>arraySafeVal($script, 'type', ''),
				'addresses'=>$addresses,
			);
		}

		$vin = arraySafeVal($tx, 'vin', array());
		if (!is_array($vin))
			$vin = array();

		return array(
			'txid'=>arraySafeVal($tx, 'txid', ''),
			'blockhash'=>arraySafeVal($tx, 'blockhash', ''),
			'blockheight'=>intval(arraySafeVal($tx, 'blockheight', 0)),
			'confirmations'=>intval(arraySafeVal($tx, 'confirmations', 0)),
			'time'=>intval(arraySafeVal($tx, 'time', arraySafeVal($tx, 'blocktime', 0))),
			'version'=>intval(arraySafeVal($tx, 'version', 0)),
			'locktime'=>intval(arraySafeVal($tx, 'locktime', 0)),
			'vin_count'=>count($vin),
			'vout_count'=>count($vout),
			'value_out'=>$total,
			'outputs'=>$outputs,
		);
	}

	private function nodeStatusData($coin, $remote)
	{
		$this->loadExplorerUtil();
		$block_time = $this->vexonBlockTime($coin);
		$chain = $remote ? vexon_explorer_rpc($remote, 'getblockchaininfo') : false;
		$peers = $remote ? vexon_explorer_rpc($remote, 'getconnectioncount') : false;
		$getwork = $remote ? vexon_explorer_rpc($remote, 'getwork') : false;
		$difficulty = is_array($chain) ? doubleval(arraySafeVal($chain, 'difficulty', 0)) : ($coin ? doubleval($coin->difficulty) : 0);
		$network_hashrate = $difficulty > 0 ? $difficulty * 4294967296.0 / $block_time : 0;

		return array(
			'connected'=>is_array($chain),
			'getwork_ready'=>is_array($getwork),
			'peers'=>is_numeric($peers) ? intval($peers) : 0,
			'chain'=>is_array($chain) ? arraySafeVal($chain, 'chain') : '',
			'blocks'=>is_array($chain) ? intval(arraySafeVal($chain, 'blocks', 0)) : ($coin ? intval($coin->block_height) : 0),
			'headers'=>is_array($chain) ? intval(arraySafeVal($chain, 'headers', 0)) : 0,
			'bestblockhash'=>is_array($chain) ? arraySafeVal($chain, 'bestblockhash', '') : '',
			'difficulty'=>$difficulty,
			'network_hashrate_hs'=>$network_hashrate,
			'network_hashrate_text'=>function_exists('vexon_explorer_hashrate') ? vexon_explorer_hashrate($difficulty, $block_time) : '',
			'verificationprogress'=>is_array($chain) ? doubleval(arraySafeVal($chain, 'verificationprogress', 0)) : 0,
			'syncheight'=>is_array($chain) ? intval(arraySafeVal($chain, 'syncheight', 0)) : 0,
			'chainwork'=>is_array($chain) ? arraySafeVal($chain, 'chainwork', '') : '',
			'block_time_seconds'=>$block_time,
		);
	}

	private function formatHashrateText($hashrate)
	{
		$this->loadExplorerUtil();
		$hashrate = doubleval($hashrate);
		if ($hashrate <= 0)
			return 'Waiting';
		return function_exists('vexon_explorer_metric') ? trim(vexon_explorer_metric($hashrate, 2).'h/s') : strval($hashrate).' h/s';
	}

	private function walletRpcStatus()
	{
		$status = array(
			'configured'=>false,
			'connected'=>false,
			'locked'=>null,
			'accounts'=>0,
			'sync_blockhash'=>'',
			'error'=>'',
		);

		if (!defined('VEXON_WALLET_RPC_HOST') || !defined('VEXON_WALLET_RPC_PORT') ||
			!defined('VEXON_WALLET_RPC_USER') || !defined('VEXON_WALLET_RPC_PASSWORD')) {
			$status['error'] = 'Wallet RPC is not configured.';
			return $status;
		}

		$status['configured'] = true;
		$status['host'] = VEXON_WALLET_RPC_HOST;
		$status['port'] = intval(VEXON_WALLET_RPC_PORT);

		if (VEXON_WALLET_RPC_USER === '' || VEXON_WALLET_RPC_PASSWORD === '') {
			$status['error'] = 'Wallet RPC credentials are empty.';
			return $status;
		}

		$wallet = new WalletRPC(VEXON_WALLET_RPC_USER, VEXON_WALLET_RPC_PASSWORD, VEXON_WALLET_RPC_HOST, VEXON_WALLET_RPC_PORT);
		$balance = $wallet->getbalance();
		if (is_array($balance)) {
			$status['connected'] = true;
			$status['sync_blockhash'] = arraySafeVal($balance, 'blockhash', '');
			$balances = arraySafeVal($balance, 'balances', array());
			$status['accounts'] = is_array($balances) ? count($balances) : 0;
		}

		$locked = $wallet->walletislocked();
		if (is_bool($locked)) {
			$status['connected'] = true;
			$status['locked'] = $locked;
		}

		$status['error'] = $wallet->error ? $wallet->error : '';
		return $status;
	}

	private function requestJsonPayload()
	{
		$body = file_get_contents('php://input');
		$payload = json_decode($body, true);
		return is_array($payload) ? $payload : array();
	}

	private function webWalletAddressBookPath()
	{
		if (defined('VEXON_WEB_WALLET_ADDRESS_BOOK') && VEXON_WEB_WALLET_ADDRESS_BOOK !== '')
			return VEXON_WEB_WALLET_ADDRESS_BOOK;
		return VEXON_LOGS.'/web-wallet-addresses.json';
	}

	private function readWebWalletAddressBook($path)
	{
		if (!is_file($path))
			return array();

		$raw = file_get_contents($path);
		$data = json_decode($raw, true);
		return is_array($data) ? $data : array();
	}

	private function writeWebWalletAddressBook($path, $data)
	{
		$dir = dirname($path);
		if (!is_dir($dir))
			@mkdir($dir, 0750, true);
		return file_put_contents($path, json_encode($data, JSON_PRETTY_PRINT), LOCK_EX) !== false;
	}

	private function webWalletRecordForKey($wallet_key)
	{
		$wallet_key = strtolower(trim((string)$wallet_key));
		if (!preg_match('/^[0-9a-f]{64}$/', $wallet_key))
			return null;

		$book = $this->readWebWalletAddressBook($this->webWalletAddressBookPath());
		$record = arraySafeVal($book, $wallet_key, array());
		return is_array($record) ? $record : null;
	}

	private function publicWebWalletRecord($record)
	{
		if (!is_array($record))
			return null;

		return array(
			'address'=>trim((string)arraySafeVal($record, 'address', '')),
			'label'=>arraySafeVal($record, 'label', ''),
			'managed'=>!!arraySafeVal($record, 'managed', true),
			'custody'=>arraySafeVal($record, 'custody', ''),
			'pool_account_id'=>intval(arraySafeVal($record, 'pool_account_id', 0)),
			'created_at'=>intval(arraySafeVal($record, 'created_at', 0)),
			'updated_at'=>intval(arraySafeVal($record, 'updated_at', 0)),
		);
	}

	private function ensureVexPoolAccount($coin, $address)
	{
		if (!$coin || $address === '')
			return null;

		$user_id = dboscalar("SELECT id FROM accounts WHERE username=:address", array(':address'=>$address));
		if (!$user_id) {
			dborun(
				"INSERT INTO accounts (username, coinsymbol, coinid, balance, donation, hostaddr, is_locked) ".
				"VALUES (:address, 'VEX', :coinid, 0, 0, 'web-wallet', 0)",
				array(':address'=>$address, ':coinid'=>$coin->id)
			);
		} else {
			dborun(
				"UPDATE accounts SET coinsymbol='VEX', coinid=:coinid, is_locked=0, ".
				"hostaddr=IFNULL(NULLIF(hostaddr,''), 'web-wallet') WHERE username=:address",
				array(':address'=>$address, ':coinid'=>$coin->id)
			);
		}

		return getdbosql('db_accounts', "username=:address", array(':address'=>$address));
	}

	private function addressData($coin, $remote, $address, $chain_limit=20)
	{
		$this->loadExplorerUtil();
		$address = trim((string)$address);
		$validation = vexon_explorer_validate_address($remote, $address);
		$user = getuserparam($address);
		$data = array(
			'address'=>$address,
			'validation'=>$validation,
			'pool_account'=>null,
			'workers'=>array(),
			'shares'=>array(),
			'earnings'=>array(),
			'payouts'=>array(),
			'chain'=>array(
				'tx_index_available'=>false,
				'received'=>0,
				'transactions'=>array(),
			),
		);

		if ($user && !$user->is_locked) {
			$t15 = time() - 15*60;
			$t24 = time() - 24*60*60;
			$total_unsold = vexon_convert_earnings_user($user, "status!=2");
			$paid24 = (double) dboscalar("SELECT SUM(amount) FROM payouts WHERE time >= $t24 AND account_id=:uid", array(':uid'=>$user->id));

			$data['pool_account'] = array(
				'id'=>intval($user->id),
				'coinid'=>intval($user->coinid),
				'coinsymbol'=>$user->coinsymbol,
				'balance'=>doubleval($user->balance),
				'immature_or_unsold'=>doubleval($total_unsold),
				'unpaid'=>doubleval($user->balance + $total_unsold),
				'paid_24h'=>$paid24,
				'minimum_payout'=>$coin ? doubleval($coin->payout_min) : (defined('VEXON_PAYMENTS_MINI') ? doubleval(VEXON_PAYMENTS_MINI) : 0.001),
			);

			$data['workers'] = dbolist("SELECT id,worker,version,'Blake256r14' AS algorithm,difficulty,subscribe,time,FROM_UNIXTIME(time) AS last_seen FROM workers WHERE userid=:uid ORDER BY time DESC LIMIT 25", array(':uid'=>$user->id));
			$data['shares'] = dbolist("SELECT valid,error,COUNT(*) AS count,SUM(difficulty) AS difficulty,MIN(time) AS first_time,MAX(time) AS last_time FROM shares WHERE userid=:uid AND algo='decred' AND time>$t24 GROUP BY valid,error ORDER BY count DESC", array(':uid'=>$user->id));
			$data['shares_15m'] = dbolist("SELECT valid,error,COUNT(*) AS count,SUM(difficulty) AS difficulty,MIN(time) AS first_time,MAX(time) AS last_time FROM shares WHERE userid=:uid AND algo='decred' AND time>$t15 GROUP BY valid,error ORDER BY count DESC", array(':uid'=>$user->id));
			$data['earnings'] = dbolist("SELECT status,COUNT(*) AS count,SUM(amount) AS amount,MIN(create_time) AS first_time,MAX(create_time) AS last_time FROM earnings WHERE userid=:uid GROUP BY status ORDER BY status", array(':uid'=>$user->id));
			$data['payouts'] = dbolist("SELECT amount,tx,completed,errmsg,time FROM payouts WHERE account_id=:uid ORDER BY id DESC LIMIT 25", array(':uid'=>$user->id));
		}

		$chain = vexon_explorer_chain_transactions($remote, $address, $chain_limit);
		$received = 0.0;
		if (arraySafeVal($chain, 'available')) {
			foreach(arraySafeVal($chain, 'transactions', array()) as $tx)
				$received += (double) arraySafeVal($tx, 'received', 0);
		}
		$data['chain'] = array(
			'tx_index_available'=>!!arraySafeVal($chain, 'available', false),
			'received'=>$received,
			'transactions'=>arraySafeVal($chain, 'transactions', array()),
		);

		return $data;
	}

	public function actionStatus()
	{
		$client_ip = arraySafeVal($_SERVER,'REMOTE_ADDR');
		$whitelisted = isAdminIP($client_ip);
		if (!$whitelisted && is_file(VEXON_LOGS.'/overloaded')) {
			header('HTTP/1.0 503 Disabled, server overloaded');
			return;
		}
		if(!$whitelisted && !LimitRequest('api-status', 10)) {
			return;
		}

		$json = controller()->memcache->get("api_status");

		if (!empty($json)) {
			echo $json;
			return;
		}

		$stats = array();
		foreach(vexon_get_algos() as $i=>$algo)
		{
			$coins = (int) controller()->memcache->get_database_count_ex("api_status_coins-$algo",
				'db_coins', "enable and visible and auto_ready and algo=:algo", array(':algo'=>$algo));

			if (!$coins) continue;

			$workers = (int) controller()->memcache->get_database_scalar("api_status_workers-$algo",
				"select COUNT(id) FROM workers WHERE algo=:algo",
				array(':algo'=>$algo)
			);

			$hashrate = controller()->memcache->get_database_scalar("api_status_hashrate-$algo",
				"select hashrate from hashrate where algo=:algo order by time desc limit 1", array(':algo'=>$algo));

			$price = controller()->memcache->get_database_scalar("api_status_price-$algo",
				"select price from hashrate where algo=:algo order by time desc limit 1", array(':algo'=>$algo));

			$price = bitcoinvaluetoa(take_vexon_fee($price/1000, $algo));

			$rental = controller()->memcache->get_database_scalar("api_status_rental-$algo",
				"select rent from hashrate where algo=:algo order by time desc limit 1", array(':algo'=>$algo));

			$rental = bitcoinvaluetoa($rental);

			$t = time() - 24*60*60;

			$avgprice = controller()->memcache->get_database_scalar("api_status_avgprice-$algo",
				"select avg(price) from hashrate where algo=:algo and time>$t", array(':algo'=>$algo));

			$avgprice = bitcoinvaluetoa(take_vexon_fee($avgprice/1000, $algo));

			$total1 = controller()->memcache->get_database_scalar("api_status_total-$algo",
				"select sum(amount*price) from blocks where category!='orphan' and time>$t and algo=:algo", array(':algo'=>$algo));

			$hashrate1 = (double) controller()->memcache->get_database_scalar("api_status_avghashrate-$algo",
				"select avg(hashrate) from hashrate where time>$t and algo=:algo", array(':algo'=>$algo));

			$algo_unit_factor = vexon_algo_mBTC_factor($algo);
			$btcmhday1 = $hashrate1 > 0 ? mbitcoinvaluetoa($total1 / $hashrate1 * 1000000 * 1000 * $algo_unit_factor) : 0;

			$fees = vexon_fee($algo);
			$port = getAlgoPort($algo);

			$stat  = array(
				"name" => $algo,
				"port" => (int) $port,
				"coins" => $coins,
				"fees" => (double) $fees,
				"hashrate" => (double) $hashrate,
				"workers" => (int) $workers,
				"estimate_current" => $price,
				"estimate_last24h" => $avgprice,
				"actual_last24h" => $btcmhday1,
				"mbtc_mh_factor" => $algo_unit_factor,
				"hashrate_last24h" => (double) $hashrate1,
			);
			if(VEXON_RENTAL) {
				$stat["rental_current"] = $rental;
			}

			$stats[$algo] = $stat;
		}

		ksort($stats);

		$json = json_encode($stats);
		echo $json;

		controller()->memcache->set("api_status", $json, 30, MEMCACHE_COMPRESSED);
	}

	public function actionPool()
	{
		if (!$this->apiGuard('api-pool', 20))
			return;

		$json = controller()->memcache->get("api_pool_vex");
		if (!empty($json)) {
			header('Content-Type: application/json');
			echo $json;
			return;
		}

		$coin = getdbosql('db_coins', "symbol='VEX'");
		$now = time();
		$t15 = $now - 15*60;
		$t24 = $now - 24*60*60;

		$accepted15 = (int) dboscalar("SELECT COUNT(*) FROM shares WHERE valid=1 AND algo='decred' AND time>$t15");
		$rejected15 = (int) dboscalar("SELECT COUNT(*) FROM shares WHERE valid=0 AND algo='decred' AND time>$t15");
		$acceptedDiff15 = (double) dboscalar("SELECT SUM(difficulty) FROM shares WHERE valid=1 AND algo='decred' AND time>$t15");
		$workers15 = function_exists('vexon_live_active_miners') ? vexon_live_active_miners('decred', 15*60) : (int) dboscalar("SELECT COUNT(DISTINCT workerid) FROM shares WHERE algo='decred' AND time>$t15");
		$workersRegistered = (int) dboscalar("SELECT COUNT(*) FROM workers WHERE algo='decred'");
		$blocks24 = $coin ? (int) dboscalar("SELECT COUNT(*) FROM blocks WHERE coin_id={$coin->id} AND time>$t24") : 0;
		$lastShare = dborow("SELECT valid,error,difficulty,share_diff,workerid,FROM_UNIXTIME(time) AS time_text,time FROM shares WHERE algo='decred' ORDER BY id DESC LIMIT 1");
		$lastBlock = $coin ? dborow("SELECT height,category,amount,difficulty,blockhash,FROM_UNIXTIME(time) AS time_text,time FROM blocks WHERE coin_id={$coin->id} ORDER BY id DESC LIMIT 1") : array();
		$failedPayouts = (int) dboscalar("SELECT COUNT(*) FROM payouts P JOIN accounts A ON A.id=P.account_id JOIN coins C ON C.id=A.coinid WHERE C.symbol='VEX' AND P.completed=0 AND P.errmsg<>''");
		$payableAccounts = $coin ? (int) dboscalar("SELECT COUNT(*) FROM accounts WHERE coinid={$coin->id} AND balance >= {$coin->payout_min}") : 0;

		$mining = getdbosql('db_mining');
		$lastPayout = $mining ? intval($mining->last_payout) : 0;
		$nextPayout = function_exists('vexon_next_payout_timestamp') ? vexon_next_payout_timestamp($lastPayout, $now) : $lastPayout + VEXON_PAYMENTS_FREQ;

		$node = array('connected' => false, 'peers' => 0);
		if ($coin) {
			$remote = new WalletRPC($coin);
			$node = $this->nodeStatusData($coin, $remote);
		}

		$poolHashrate = $acceptedDiff15 > 0 ? $acceptedDiff15 * vexon_hashrate_constant('decred') / (15*60) : 0;

		$data = array(
			'coin' => array(
				'name' => $coin ? $coin->name : 'Vexon',
				'symbol' => 'VEX',
				'height' => $coin ? intval($coin->block_height) : 0,
				'difficulty' => $coin ? doubleval($coin->difficulty) : 0,
				'auto_ready' => $coin ? intval($coin->auto_ready) : 0,
				'enabled' => $coin ? intval($coin->enable) : 0,
				'payout_min' => $coin ? doubleval($coin->payout_min) : 0,
				'mature_blocks' => $coin ? intval($coin->mature_blocks) : 0,
				'txfee' => $coin ? doubleval($coin->txfee) : 0,
				'master_wallet' => $coin ? $coin->master_wallet : '',
			),
			'mining' => array(
				'algorithm' => 'Blake256r14',
				'stratum_host' => defined('VEXON_STRATUM_URL') ? VEXON_STRATUM_URL : 'pool.vexonus.com',
				'stratum_port' => getAlgoPort('decred'),
				'accepted_shares_15m' => $accepted15,
				'rejected_shares_15m' => $rejected15,
				'accepted_difficulty_15m' => $acceptedDiff15,
				'pool_hashrate_hs' => $poolHashrate,
				'active_workers_15m' => $workers15,
				'registered_workers' => $workersRegistered,
				'blocks_24h' => $blocks24,
				'last_share' => $lastShare ? $lastShare : array(),
				'last_block' => $lastBlock ? $lastBlock : array(),
			),
			'payout' => array(
				'minimum' => defined('VEXON_PAYMENTS_MINI') ? doubleval(VEXON_PAYMENTS_MINI) : 0.001,
				'schedule' => defined('VEXON_PAYOUT_SCHEDULE_LABEL') ? VEXON_PAYOUT_SCHEDULE_LABEL : 'Daily at 08:00 CST',
				'note' => defined('VEXON_PAYOUT_NOTE') ? VEXON_PAYOUT_NOTE : '',
				'last_payout_time' => $lastPayout,
				'next_payout_time' => $nextPayout,
				'next_payout_iso' => date('c', $nextPayout),
				'payable_accounts' => $payableAccounts,
				'failed_payouts' => $failedPayouts,
			),
			'node' => $node,
			'generated_at' => $now,
		);

		$json = json_encode($data);
		header('Content-Type: application/json');
		echo $json;
		controller()->memcache->set("api_pool_vex", $json, 15, MEMCACHE_COMPRESSED);
	}

	public function actionDiagnostics()
	{
		if (!$this->apiGuard('api-diagnostics', 30))
			return;

		$coin = getdbosql('db_coins', "symbol='VEX'");
		$now = time();
		$algo = 'decred';
		$remote = $coin ? new WalletRPC($coin) : null;
		$node = $coin ? $this->nodeStatusData($coin, $remote) : array(
			'connected'=>false,
			'getwork_ready'=>false,
			'peers'=>0,
			'blocks'=>0,
			'difficulty'=>0,
			'network_hashrate_hs'=>0,
			'network_hashrate_text'=>'Waiting',
			'block_time_seconds'=>defined('VEXON_BLOCK_TIME_ESTIMATE') ? intval(VEXON_BLOCK_TIME_ESTIMATE) : 150,
		);

		$windows = array(
			'5m'=>5*60,
			'15m'=>15*60,
			'60m'=>60*60,
			'24h'=>24*60*60,
		);
		$shares = array();
		foreach($windows as $label=>$window) {
			$since = $now - $window;
			$rows = dbolist("SELECT valid,error,COUNT(*) AS count,SUM(difficulty) AS difficulty,MIN(time) AS first_time,MAX(time) AS last_time FROM shares WHERE algo=:algo AND time>$since GROUP BY valid,error ORDER BY count DESC", array(':algo'=>$algo));
			$summary = array(
				'accepted'=>0,
				'rejected'=>0,
				'accepted_difficulty'=>0.0,
				'rejected_difficulty'=>0.0,
				'first_time'=>0,
				'last_time'=>0,
				'workers_from_shares'=>0,
				'workers_seen'=>0,
				'pool_hashrate_hs'=>0,
				'pool_hashrate_text'=>'Waiting',
				'rows'=>$rows,
			);
			foreach($rows as $row) {
				$valid = intval(arraySafeVal($row, 'valid', 0));
				$count = intval(arraySafeVal($row, 'count', 0));
				$difficulty = doubleval(arraySafeVal($row, 'difficulty', 0));
				$first = intval(arraySafeVal($row, 'first_time', 0));
				$last = intval(arraySafeVal($row, 'last_time', 0));
				if ($valid) {
					$summary['accepted'] += $count;
					$summary['accepted_difficulty'] += $difficulty;
				} else {
					$summary['rejected'] += $count;
					$summary['rejected_difficulty'] += $difficulty;
				}
				if ($first && (!$summary['first_time'] || $first < $summary['first_time']))
					$summary['first_time'] = $first;
				if ($last && $last > $summary['last_time'])
					$summary['last_time'] = $last;
			}

			$summary['workers_from_shares'] = intval(dboscalar("SELECT COUNT(DISTINCT CONCAT(userid, ':', IFNULL(workerid, 0))) FROM shares WHERE algo=:algo AND time>$since", array(':algo'=>$algo)));
			$summary['workers_seen'] = intval(dboscalar("SELECT COUNT(DISTINCT id) FROM workers WHERE algo=:algo AND time>$since", array(':algo'=>$algo)));
			$summary['active_miners'] = max($summary['workers_from_shares'], $summary['workers_seen']);
			$summary['pool_hashrate_hs'] = $summary['accepted_difficulty'] > 0 ? $summary['accepted_difficulty'] * vexon_hashrate_constant($algo) / $window : 0;
			$summary['pool_hashrate_text'] = $this->formatHashrateText($summary['pool_hashrate_hs']);
			$shares[$label] = $summary;
		}

		$lastShare = dborow("SELECT id,userid,coinid,workerid,valid,error,difficulty,share_diff,time,FROM_UNIXTIME(time) AS time_text FROM shares WHERE algo=:algo ORDER BY id DESC LIMIT 1", array(':algo'=>$algo));
		$lastWorker = dborow("SELECT id,userid,ip,name,worker,version,difficulty,time,FROM_UNIXTIME(time) AS time_text FROM workers WHERE algo=:algo ORDER BY time DESC LIMIT 1", array(':algo'=>$algo));
		$coin_id = $coin ? intval($coin->id) : 0;
		$t24 = $now - 24*60*60;
		$blocks24 = $coin ? intval(dboscalar("SELECT COUNT(*) FROM blocks WHERE coin_id=$coin_id AND time>$t24")) : 0;
		$lastBlock = $coin ? dborow("SELECT id,height,category,amount,difficulty,blockhash,time,FROM_UNIXTIME(time) AS time_text FROM blocks WHERE coin_id=$coin_id ORDER BY id DESC LIMIT 1") : array();
		$blockCategories = $coin ? dbolist("SELECT category,COUNT(*) AS count,SUM(amount) AS amount,MIN(time) AS first_time,MAX(time) AS last_time FROM blocks WHERE coin_id=$coin_id AND time>$t24 GROUP BY category ORDER BY count DESC") : array();
		$payableAccounts = $coin ? intval(dboscalar("SELECT COUNT(*) FROM accounts WHERE coinid=$coin_id AND balance >= ".doubleval($coin->payout_min))) : 0;
		$failedPayouts = intval(dboscalar("SELECT COUNT(*) FROM payouts P JOIN accounts A ON A.id=P.account_id JOIN coins C ON C.id=A.coinid WHERE C.symbol='VEX' AND P.completed=0 AND P.errmsg<>''"));

		$poolHashrate = doubleval(arraySafeVal(arraySafeVal($shares, '15m', array()), 'pool_hashrate_hs', 0));
		$networkDifficulty = doubleval(arraySafeVal($node, 'difficulty', $coin ? $coin->difficulty : 0));
		$expectedBlockSeconds = ($poolHashrate > 0 && $networkDifficulty > 0) ? intval($networkDifficulty * 4294967296.0 / $poolHashrate) : 0;

		$critical = array();
		$warnings = array();
		if (!$coin)
			$critical[] = 'VEX coin row is missing.';
		if ($coin && !intval($coin->enable))
			$critical[] = 'VEX coin is disabled.';
		if ($coin && !intval($coin->auto_ready))
			$critical[] = 'VEX coin auto_ready is not enabled.';
		if (!arraySafeVal($node, 'connected', false))
			$critical[] = 'Vexon node RPC is not connected.';
		if (!arraySafeVal($node, 'getwork_ready', false))
			$critical[] = 'Vexon node cannot create getwork templates.';
		if (intval(arraySafeVal($node, 'peers', 0)) <= 0)
			$warnings[] = 'Node has no peers; keep the local peer or public seed node online.';
		if (intval(arraySafeVal($shares['15m'], 'active_miners', 0)) <= 0)
			$warnings[] = 'No active workers were seen in the last 15 minutes.';
		if (intval(arraySafeVal($shares['15m'], 'active_miners', 0)) > 0 && intval(arraySafeVal($shares['15m'], 'accepted', 0)) <= 0)
			$warnings[] = 'Workers are connecting, but no accepted shares were recorded in the last 15 minutes.';
		if ($lastShare && intval(arraySafeVal($lastShare, 'time', 0)) > 0 && $now - intval(arraySafeVal($lastShare, 'time', 0)) > 15*60)
			$warnings[] = 'The latest accepted/rejected share is older than 15 minutes.';
		if ($coin && doubleval($coin->payout_min) <= 0)
			$warnings[] = 'VEX payout minimum is not configured.';

		$this->jsonOut(array(
			'ok'=>count($critical) === 0,
			'type'=>'diagnostics',
			'readiness'=>array(
				'coin_exists'=>!!$coin,
				'coin_enabled'=>$coin ? !!intval($coin->enable) : false,
				'coin_auto_ready'=>$coin ? !!intval($coin->auto_ready) : false,
				'node_connected'=>!!arraySafeVal($node, 'connected', false),
				'getwork_ready'=>!!arraySafeVal($node, 'getwork_ready', false),
				'node_has_peer'=>intval(arraySafeVal($node, 'peers', 0)) > 0,
				'workers_seen_15m'=>intval(arraySafeVal($shares['15m'], 'active_miners', 0)) > 0,
				'accepted_shares_15m'=>intval(arraySafeVal($shares['15m'], 'accepted', 0)) > 0,
				'payout_configured'=>$coin ? doubleval($coin->payout_min) > 0 : false,
			),
			'critical'=>$critical,
			'warnings'=>$warnings,
			'coin'=>array(
				'id'=>$coin_id,
				'name'=>$coin ? $coin->name : 'Vexon',
				'symbol'=>'VEX',
				'enabled'=>$coin ? intval($coin->enable) : 0,
				'auto_ready'=>$coin ? intval($coin->auto_ready) : 0,
				'algo'=>'Blake256r14',
				'compatibility_algo'=>$algo,
				'height'=>$coin ? intval($coin->block_height) : 0,
				'difficulty'=>$coin ? doubleval($coin->difficulty) : 0,
				'payout_min'=>$coin ? doubleval($coin->payout_min) : 0,
				'mature_blocks'=>$coin ? intval($coin->mature_blocks) : 0,
			),
			'stratum'=>array(
				'host'=>defined('VEXON_STRATUM_URL') ? VEXON_STRATUM_URL : 'pool.vexonus.com',
				'port'=>getAlgoPort($algo),
				'algorithm'=>'Blake256r14',
			),
			'node'=>$node,
			'mining'=>array(
				'shares'=>$shares,
				'pool_hashrate_hs'=>$poolHashrate,
				'pool_hashrate_text'=>$this->formatHashrateText($poolHashrate),
				'expected_block_seconds'=>$expectedBlockSeconds,
				'expected_block_time'=>$expectedBlockSeconds > 0 ? sectoa2($expectedBlockSeconds) : 'Waiting for accepted shares',
				'workers_registered'=>intval(dboscalar("SELECT COUNT(*) FROM workers WHERE algo=:algo", array(':algo'=>$algo))),
				'last_worker'=>$lastWorker ? $lastWorker : array(),
				'last_share'=>$lastShare ? $lastShare : array(),
			),
			'blocks'=>array(
				'found_24h'=>$blocks24,
				'categories_24h'=>$blockCategories,
				'last_block'=>$lastBlock ? $lastBlock : array(),
			),
			'payout'=>array(
				'minimum'=>$coin ? doubleval($coin->payout_min) : (defined('VEXON_PAYMENTS_MINI') ? doubleval(VEXON_PAYMENTS_MINI) : 0.001),
				'schedule'=>defined('VEXON_PAYOUT_SCHEDULE_LABEL') ? VEXON_PAYOUT_SCHEDULE_LABEL : 'Daily at 08:00 CST',
				'payable_accounts'=>$payableAccounts,
				'failed_payouts'=>$failedPayouts,
			),
			'generated_at'=>$now,
		), 'api_diagnostics_vex', 10);
	}

	public function actionExplorer()
	{
		if (!$this->apiGuard('api-explorer', 30))
			return;

		$this->loadExplorerUtil();
		$coin = getdbosql('db_coins', "symbol='VEX'");
		if (!$coin) {
			$this->jsonOut(array('ok'=>false, 'error'=>'VEX coin row is missing.'));
			return;
		}

		$remote = new WalletRPC($coin);
		$node = $this->nodeStatusData($coin, $remote);
		$q = trim((string)getparam('q'));
		$height = trim((string)getparam('height'));
		$hash = trim((string)getparam('hash'));
		$txid = trim((string)getparam('txid'));
		$address = trim((string)getparam('address'));

		if ($address !== '') {
			$this->jsonOut(array(
				'ok'=>true,
				'type'=>'address',
				'node'=>$node,
				'data'=>$this->addressData($coin, $remote, $address, 25),
				'generated_at'=>time(),
			));
			return;
		}

		if ($height !== '')
			$q = $height;
		if ($hash !== '')
			$q = $hash;
		if ($txid !== '')
			$q = $txid;

		if ($q === '') {
			$blocks = array();
			$tip = intval(arraySafeVal($node, 'blocks', 0));
			for($h=$tip; $h>=max(0, $tip-9); $h--) {
				$block_hash = vexon_explorer_rpc($remote, 'getblockhash', array($h));
				if (!$block_hash)
					continue;
				$block = vexon_explorer_rpc($remote, 'getblock', array($block_hash));
				if (is_array($block))
					$blocks[] = $this->summarizeBlock($block);
			}
			$this->jsonOut(array(
				'ok'=>true,
				'type'=>'overview',
				'node'=>$node,
				'latest_blocks'=>$blocks,
				'generated_at'=>time(),
			), 'api_explorer_overview_vex', 10);
			return;
		}

		if (ctype_digit($q)) {
			$block_hash = vexon_explorer_rpc($remote, 'getblockhash', array(intval($q)));
			$block = $block_hash ? vexon_explorer_rpc($remote, 'getblock', array($block_hash)) : false;
			$this->jsonOut(array(
				'ok'=>is_array($block),
				'type'=>'block',
				'query'=>$q,
				'node'=>$node,
				'data'=>is_array($block) ? $this->summarizeBlock($block) : null,
				'error'=>is_array($block) ? '' : 'Block height not found.',
				'generated_at'=>time(),
			));
			return;
		}

		if (vexon_explorer_is_address($q)) {
			$this->jsonOut(array(
				'ok'=>true,
				'type'=>'address',
				'query'=>$q,
				'node'=>$node,
				'data'=>$this->addressData($coin, $remote, $q, 25),
				'generated_at'=>time(),
			));
			return;
		}

		if (vexon_explorer_is_hash($q)) {
			$block = vexon_explorer_rpc($remote, 'getblock', array($q));
			if (is_array($block)) {
				$this->jsonOut(array(
					'ok'=>true,
					'type'=>'block',
					'query'=>$q,
					'node'=>$node,
					'data'=>$this->summarizeBlock($block),
					'generated_at'=>time(),
				));
				return;
			}

			$tx = vexon_explorer_rpc($remote, 'getrawtransaction', array($q, 1));
			$this->jsonOut(array(
				'ok'=>is_array($tx),
				'type'=>'transaction',
				'query'=>$q,
				'node'=>$node,
				'data'=>is_array($tx) ? $this->summarizeTransaction($tx) : null,
				'error'=>is_array($tx) ? '' : 'Hash was not found as a block or transaction.',
				'generated_at'=>time(),
			));
			return;
		}

		$this->jsonOut(array(
			'ok'=>false,
			'type'=>'unknown',
			'query'=>$q,
			'node'=>$node,
			'error'=>'Enter a VEX address, block height, block hash, or transaction id.',
			'generated_at'=>time(),
		));
	}

	public function actionAddress()
	{
		if (!$this->apiGuard('api-address', 30))
			return;

		$this->loadExplorerUtil();
		$coin = getdbosql('db_coins', "symbol='VEX'");
		if (!$coin) {
			$this->jsonOut(array('ok'=>false, 'error'=>'VEX coin row is missing.'));
			return;
		}

		$address = trim((string)getparam('address'));
		if ($address === '')
			$address = trim((string)getparam('q'));
		if ($address === '') {
			$this->jsonOut(array('ok'=>false, 'error'=>'Missing address parameter.'));
			return;
		}

		$remote = new WalletRPC($coin);
		$this->jsonOut(array(
			'ok'=>true,
			'type'=>'address',
			'node'=>$this->nodeStatusData($coin, $remote),
			'data'=>$this->addressData($coin, $remote, $address, 50),
			'generated_at'=>time(),
		));
	}

	public function actionWalletsync()
	{
		if (!$this->apiGuard('api-walletsync', 30))
			return;

		$this->loadExplorerUtil();
		$coin = getdbosql('db_coins', "symbol='VEX'");
		if (!$coin) {
			$this->jsonOut(array('ok'=>false, 'type'=>'walletsync', 'error'=>'VEX coin row is missing.'));
			return;
		}

		$remote = new WalletRPC($coin);
		$node = $this->nodeStatusData($coin, $remote);
		$payload = $_SERVER['REQUEST_METHOD'] === 'POST' ? $this->requestJsonPayload() : array();
		$wallet_key = strtolower(trim((string)arraySafeVal($payload, 'wallet_key', getparam('wallet_key'))));
		$wallet_record = null;
		$managed_address = '';
		$wallet_key_status = 'not_provided';

		if ($wallet_key !== '') {
			if (!preg_match('/^[0-9a-f]{64}$/', $wallet_key)) {
				$wallet_key_status = 'invalid';
			} else {
				$wallet_record = $this->webWalletRecordForKey($wallet_key);
				if (is_array($wallet_record)) {
					$managed_address = trim((string)arraySafeVal($wallet_record, 'address', ''));
					$wallet_key_status = preg_match('/^Vs[1-9A-HJ-NP-Za-km-z]{25,45}$/', $managed_address) ? 'found' : 'address_missing';
				} else {
					$wallet_key_status = 'not_found';
				}
			}
		}

		$address = trim((string)getparam('address'));
		if ($address === '' && $managed_address !== '')
			$address = $managed_address;
		$address_data = null;

		if ($address !== '') {
			$address_full = $this->addressData($coin, $remote, $address, 10);
			$pool = arraySafeVal($address_full, 'pool_account', array());
			$chain = arraySafeVal($address_full, 'chain', array());
			$transactions = is_array($chain) ? arraySafeVal($chain, 'transactions', array()) : array();
			if (!is_array($transactions))
				$transactions = array();
			$address_data = array(
				'address'=>$address,
				'validation'=>arraySafeVal($address_full, 'validation', array()),
				'pool_account_found'=>is_array($pool) && !empty($pool),
				'pool_unpaid'=>is_array($pool) ? doubleval(arraySafeVal($pool, 'unpaid', 0)) : 0,
				'chain_received'=>is_array($chain) ? doubleval(arraySafeVal($chain, 'received', 0)) : 0,
				'tx_index_available'=>is_array($chain) ? !!arraySafeVal($chain, 'tx_index_available', false) : false,
				'transaction_count'=>count($transactions),
			);
		}

		$this->jsonOut(array(
			'ok'=>true,
			'type'=>'walletsync',
			'node'=>$node,
			'wallet_rpc'=>$this->walletRpcStatus(),
			'capabilities'=>array(
				'address_lookup'=>true,
				'pool_account_lookup'=>true,
				'managed_chain_address_issue'=>true,
				'signed_raw_transaction_broadcast'=>true,
				'server_side_private_key_signing'=>false,
			),
			'wallet_key_status'=>$wallet_key_status,
			'managed_address'=>$managed_address,
			'address_record'=>$this->publicWebWalletRecord($wallet_record),
			'address'=>$address_data,
			'generated_at'=>time(),
		));
	}

	public function actionWalletaddress()
	{
		if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
			$this->jsonOut(array('ok'=>true, 'type'=>'walletaddress-preflight'));
			return;
		}

		if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
			header('HTTP/1.1 405 Method Not Allowed');
			$this->jsonOut(array(
				'ok'=>false,
				'type'=>'walletaddress',
				'error'=>'Use POST. The request body must contain a local wallet_key.',
			));
			return;
		}

		if (!$this->apiGuard('api-walletaddress', 8))
			return;

		$payload = $this->requestJsonPayload();
		$wallet_key = strtolower(trim((string)arraySafeVal($payload, 'wallet_key', arraySafeVal($_POST, 'wallet_key', ''))));
		$label = trim((string)arraySafeVal($payload, 'label', 'web-wallet'));
		$label = preg_replace('/[^A-Za-z0-9_. -]/', '', $label);
		$label = substr($label ? $label : 'web-wallet', 0, 64);

		if (!preg_match('/^[0-9a-f]{64}$/', $wallet_key)) {
			$this->jsonOut(array(
				'ok'=>false,
				'type'=>'walletaddress',
				'error'=>'Invalid wallet_key. Create or import the wallet again.',
				'generated_at'=>time(),
			));
			return;
		}

		$this->loadExplorerUtil();
		$coin = getdbosql('db_coins', "symbol='VEX'");
		if (!$coin) {
			$this->jsonOut(array('ok'=>false, 'type'=>'walletaddress', 'error'=>'VEX coin row is missing.'));
			return;
		}

		$path = $this->webWalletAddressBookPath();
		$lock_path = $path.'.lock';
		$lock_dir = dirname($lock_path);
		if (!is_dir($lock_dir))
			@mkdir($lock_dir, 0750, true);
		$lock = @fopen($lock_path, 'c+');
		if (!$lock || !flock($lock, LOCK_EX)) {
			$this->jsonOut(array(
				'ok'=>false,
				'type'=>'walletaddress',
				'error'=>'Wallet address registry is temporarily unavailable.',
				'generated_at'=>time(),
			));
			return;
		}

		$book = $this->readWebWalletAddressBook($path);
		$record = arraySafeVal($book, $wallet_key, array());
		$existing = false;
		$address = is_array($record) ? trim((string)arraySafeVal($record, 'address', '')) : '';

		if ($address !== '' && preg_match('/^Vs[1-9A-HJ-NP-Za-km-z]{25,45}$/', $address)) {
			$existing = true;
		} else {
			if (!defined('VEXON_WALLET_RPC_HOST') || !defined('VEXON_WALLET_RPC_PORT') ||
				!defined('VEXON_WALLET_RPC_USER') || !defined('VEXON_WALLET_RPC_PASSWORD') ||
				VEXON_WALLET_RPC_USER === '' || VEXON_WALLET_RPC_PASSWORD === '') {
				flock($lock, LOCK_UN);
				fclose($lock);
				$this->jsonOut(array(
					'ok'=>false,
					'type'=>'walletaddress',
					'error'=>'Wallet RPC is not configured.',
					'wallet_rpc'=>$this->walletRpcStatus(),
					'generated_at'=>time(),
				));
				return;
			}

			$wallet = new WalletRPC(VEXON_WALLET_RPC_USER, VEXON_WALLET_RPC_PASSWORD, VEXON_WALLET_RPC_HOST, VEXON_WALLET_RPC_PORT);
			$address = $wallet->getnewaddress();
			if (!is_string($address) || !preg_match('/^Vs[1-9A-HJ-NP-Za-km-z]{25,45}$/', trim($address))) {
				$error = $wallet && !empty($wallet->error) ? $wallet->error : 'Wallet RPC did not return a Vexon chain address.';
				flock($lock, LOCK_UN);
				fclose($lock);
				$this->jsonOut(array(
					'ok'=>false,
					'type'=>'walletaddress',
					'error'=>$error,
					'wallet_rpc'=>$this->walletRpcStatus(),
					'generated_at'=>time(),
				));
				return;
			}
			$address = trim($address);
		}

		$remote = new WalletRPC($coin);
		$validation = vexon_explorer_validate_address($remote, $address);
		if (!arraySafeVal($validation, 'isvalid', false)) {
			flock($lock, LOCK_UN);
			fclose($lock);
			$this->jsonOut(array(
				'ok'=>false,
				'type'=>'walletaddress',
				'error'=>'Vexon node rejected the generated address.',
				'address'=>$address,
				'validation'=>$validation,
				'generated_at'=>time(),
			));
			return;
		}

		$pool_account = $this->ensureVexPoolAccount($coin, $address);
		$book[$wallet_key] = array(
			'address'=>$address,
			'label'=>$label,
			'managed'=>true,
			'custody'=>'server_wallet_rpc',
			'pool_account_id'=>$pool_account ? intval($pool_account->id) : 0,
			'created_at'=>is_array($record) && intval(arraySafeVal($record, 'created_at', 0)) ? intval(arraySafeVal($record, 'created_at', 0)) : time(),
			'updated_at'=>time(),
		);

		$written = $this->writeWebWalletAddressBook($path, $book);
		flock($lock, LOCK_UN);
		fclose($lock);

		if (!$written) {
			$this->jsonOut(array(
				'ok'=>false,
				'type'=>'walletaddress',
				'error'=>'Unable to save wallet address registry.',
				'generated_at'=>time(),
			));
			return;
		}

		$this->jsonOut(array(
			'ok'=>true,
			'type'=>'walletaddress',
			'address'=>$address,
			'existing'=>$existing,
			'managed'=>true,
			'custody'=>'server_wallet_rpc',
			'pool_account'=>array(
				'id'=>$pool_account ? intval($pool_account->id) : 0,
				'username'=>$address,
				'coinsymbol'=>'VEX',
			),
			'validation'=>$validation,
			'wallet_rpc'=>$this->walletRpcStatus(),
			'generated_at'=>time(),
		));
	}

	public function actionBroadcast()
	{
		if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
			$this->jsonOut(array('ok'=>true, 'type'=>'broadcast-preflight'));
			return;
		}

		if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
			header('HTTP/1.1 405 Method Not Allowed');
			$this->jsonOut(array(
				'ok'=>false,
				'type'=>'broadcast',
				'error'=>'Use POST with a signed raw transaction hex payload.',
			));
			return;
		}

		if (!$this->apiGuard('api-broadcast', 6))
			return;

		$body = file_get_contents('php://input');
		$payload = json_decode($body, true);
		if (!is_array($payload))
			$payload = array();

		$rawtx = trim((string)arraySafeVal($payload, 'rawtx', arraySafeVal($_POST, 'rawtx', '')));
		if ($rawtx === '')
			$rawtx = trim((string)getparam('rawtx'));
		$rawtx = preg_replace('/\s+/', '', $rawtx);
		$length = strlen($rawtx);

		if ($rawtx === '' || $length < 80 || $length > 400000 || ($length % 2) !== 0 || !ctype_xdigit($rawtx)) {
			$this->jsonOut(array(
				'ok'=>false,
				'type'=>'broadcast',
				'error'=>'Invalid raw transaction hex.',
				'bytes'=>intval($length / 2),
				'generated_at'=>time(),
			));
			return;
		}

		$coin = getdbosql('db_coins', "symbol='VEX'");
		if (!$coin) {
			$this->jsonOut(array('ok'=>false, 'type'=>'broadcast', 'error'=>'VEX coin row is missing.'));
			return;
		}

		$this->loadExplorerUtil();
		$remote = new WalletRPC($coin);
		$node = $this->nodeStatusData($coin, $remote);
		if (!arraySafeVal($node, 'connected', false)) {
			$this->jsonOut(array(
				'ok'=>false,
				'type'=>'broadcast',
				'error'=>'Vexon node RPC is not available.',
				'node'=>$node,
				'generated_at'=>time(),
			));
			return;
		}

		$txid = vexon_explorer_rpc($remote, 'sendrawtransaction', array($rawtx));
		if (!is_string($txid) || $txid === '') {
			$error = $remote && !empty($remote->error) ? $remote->error : 'Transaction was rejected by the node.';
			$this->jsonOut(array(
				'ok'=>false,
				'type'=>'broadcast',
				'error'=>$error,
				'node'=>$node,
				'bytes'=>intval($length / 2),
				'generated_at'=>time(),
			));
			return;
		}

		debuglog("VEX broadcast accepted txid=$txid bytes=".intval($length / 2));
		$this->jsonOut(array(
			'ok'=>true,
			'type'=>'broadcast',
			'txid'=>$txid,
			'bytes'=>intval($length / 2),
			'node'=>$node,
			'generated_at'=>time(),
		));
	}

	public function actionCurrencies()
	{
		$client_ip = arraySafeVal($_SERVER,'REMOTE_ADDR');
		$whitelisted = isAdminIP($client_ip);
		if (!$whitelisted && is_file(VEXON_LOGS.'/overloaded')) {
			header('HTTP/1.0 503 Disabled, server overloaded');
			return;
		}
		if(!$whitelisted && !LimitRequest('api-currencies', 10)) {
			return;
		}

		$json = controller()->memcache->get("api_currencies");
		if (empty($json)) {

			$data = array();
			$coins = getdbolist('db_coins', "enable AND visible AND auto_ready AND IFNULL(algo,'PoS')!='PoS' ORDER BY symbol");
			foreach ($coins as $coin)
			{
				$symbol = $coin->symbol;

				$last = dborow("SELECT height, time FROM blocks ".
					"WHERE coin_id=:id AND category IN ('immature','generate') ORDER BY height DESC LIMIT 1",
					array(':id'=>$coin->id)
				);
				$lastblock = (int) arraySafeVal($last,'height');
				$timesincelast = $timelast = (int) arraySafeVal($last,'time');
				if ($timelast > 0) $timesincelast = time() - $timelast;

				$workers = (int) dboscalar("SELECT count(W.userid) AS workers FROM workers W ".
					"INNER JOIN accounts A ON A.id = W.userid ".
					"WHERE W.algo=:algo AND A.coinid IN (:id, 6)", // 6: btc id
					array(':algo'=>$coin->algo, ':id'=>$coin->id)
				);

				$since = $timelast ? $timelast : time() - 60*60;
				$shares = dborow("SELECT count(id) AS shares, SUM(difficulty) AS coin_hr FROM shares WHERE time>$since AND algo=:algo AND coinid IN (0,:id)",
					array(':id'=>$coin->id,':algo'=>$coin->algo)
				);

				$t24 = time() - 24*60*60;
				$res24h = controller()->memcache->get_database_row("history_item2-{$coin->id}-{$coin->algo}",
					"SELECT COUNT(id) as a, SUM(amount*price) as b FROM blocks ".
					"WHERE coin_id=:id AND NOT category IN ('orphan','stake','generated') AND time>$t24 AND algo=:algo",
					array(':id'=>$coin->id, ':algo'=>$coin->algo)
				);

				// Coin hashrate, we only store the hashrate per algo in the db,
				// we need to compute the % of the coin compared to others with the same algo
				if ($workers > 0) {

					$algohr = (double) dboscalar("SELECT SUM(difficulty) AS algo_hr FROM shares WHERE time>$since AND algo=:algo",array(':algo'=>$coin->algo));
					$factor = ($algohr > 0 && !empty($shares)) ? (double) $shares['coin_hr'] / $algohr : 1.;
					$algo_hashrate = controller()->memcache->get_database_scalar("api_status_hashrate-{$coin->algo}",
						"SELECT hashrate FROM hashrate WHERE algo=:algo ORDER BY time DESC LIMIT 1", array(':algo'=>$coin->algo)
					);

				} else {
					$factor = $algo_hashrate = 0;
				}

				$btcmhd = vexon_profitability($coin);
				$btcmhd = mbitcoinvaluetoa($btcmhd);

				$data[$symbol] = array(
					'algo' => $coin->algo,
					'port' => getAlgoPort($coin->algo),
					'name' => $coin->name,
					'height' => (int) $coin->block_height,
					'workers' => $workers,
					'shares' =>  (int) arraySafeVal($shares,'shares'),
					'hashrate' => round($factor * $algo_hashrate),
					'estimate' => $btcmhd,
					//'percent' => round($factor * 100, 1),
					'24h_blocks' => (int) arraySafeVal($res24h,'a'),
					'24h_btc' => round(arraySafeVal($res24h,'b',0), 8),
					'lastblock' => $lastblock,
					'timesincelast' => $timesincelast,
				);

				if (!empty($coin->symbol2))
					$data[$symbol]['symbol'] = $coin->symbol2;
			}
			$json = json_encode($data);
			controller()->memcache->set("api_currencies", $json, 15, MEMCACHE_COMPRESSED);
		}

		echo str_replace("},","},\n", $json);
	}

	public function actionWallet()
	{
		if(!LimitRequest('api-wallet', 10)) {
			return;
		}
		if (is_file(VEXON_LOGS.'/overloaded')) {
			header('HTTP/1.0 503 Disabled, server overloaded');
			return;
		}
		$wallet = getparam('address');

		$user = getuserparam($wallet);
		if(!$user || $user->is_locked) return;

		$total_unsold = vexon_convert_earnings_user($user, "status!=2");

		$t = time() - 24*60*60;
		$total_paid = bitcoinvaluetoa(controller()->memcache->get_database_scalar("api_wallet_paid-".$user->id,
			"SELECT SUM(amount) FROM payouts WHERE time >= $t AND account_id=".$user->id));

		$balance = bitcoinvaluetoa($user->balance);
		$total_unpaid = bitcoinvaluetoa($balance + $total_unsold);
		$total_earned = bitcoinvaluetoa($total_unpaid + $total_paid);

		$coin = getdbo('db_coins', $user->coinid);
		if(!$coin) return;

		echo "{";
		echo "\"currency\": \"{$coin->symbol}\", ";
		echo "\"unsold\": $total_unsold, ";
		echo "\"balance\": $balance, ";
		echo "\"unpaid\": $total_unpaid, ";
		echo "\"paid24h\": $total_paid, ";
		echo "\"total\": $total_earned";
		echo "}";
	}

	public function actionWalletEx()
	{
		$wallet = getparam('address');
		if (is_file(VEXON_LOGS.'/overloaded')) {
			header('HTTP/1.0 503 Disabled, server overloaded');
			return;
		}
		if(!LimitRequest('api-wallet', 60)) {
			return;
		}

		$user = getuserparam($wallet);
		if(!$user || $user->is_locked) return;

		$total_unsold = vexon_convert_earnings_user($user, "status!=2");

		$t = time() - 24*60*60;
		$total_paid = bitcoinvaluetoa(controller()->memcache->get_database_scalar("api_wallet_paid-".$user->id,
			"SELECT SUM(amount) FROM payouts WHERE time >= $t AND account_id=".$user->id));

		$balance = bitcoinvaluetoa($user->balance);
		$total_unpaid = bitcoinvaluetoa($balance + $total_unsold);
		$total_earned = bitcoinvaluetoa($total_unpaid + $total_paid);

		$coin = getdbo('db_coins', $user->coinid);
		if(!$coin) return;

		echo "{";
		echo "\"currency\": ".json_encode($coin->symbol).", ";
		echo "\"unsold\": $total_unsold, ";
		echo "\"balance\": $balance, ";
		echo "\"unpaid\": $total_unpaid, ";
		echo "\"paid24h\": $total_paid, ";
		echo "\"total\": $total_earned, ";

		echo "\"miners\": ";
		echo "[";

		$workers = getdbolist('db_workers', "userid={$user->id} ORDER BY password");
		foreach($workers as $i=>$worker)
		{
			$user_rate1 = vexon_worker_rate($worker->id, $worker->algo);
			$user_rate1_bad = vexon_worker_rate_bad($worker->id, $worker->algo);

			if($i) echo ", ";

			echo "{";
			echo "\"version\": ".json_encode($worker->version).", ";
			echo "\"password\": ".json_encode($worker->password).", ";
			echo "\"ID\": ".json_encode($worker->worker).", ";
			echo "\"algo\": \"{$worker->algo}\", ";
			echo "\"difficulty\": ".doubleval($worker->difficulty).", ";
			echo "\"subscribe\": ".intval($worker->subscribe).", ";
			echo "\"accepted\": ".round($user_rate1,3).", ";
			echo "\"rejected\": ".round($user_rate1_bad,3);
			echo "}";
		}

		echo "]";

		if(VEXON_API_PAYOUTS)
		{
			$json_payouts = controller()->memcache->get("api_payouts-".$user->id);
			if (empty($json_payouts)) {
				$json_payouts = ",\"payouts\": ";
				$json_payouts .= "[";
				$list = getdbolist('db_payouts', "account_id={$user->id} AND completed>0 AND tx IS NOT NULL AND time >= ".(time() - VEXON_API_PAYOUTS_PERIOD)." ORDER BY time DESC");
				foreach($list as $j => $payout)
				{
					if($j) $json_payouts .= ", ";
					$json_payouts .= "{";
					$json_payouts .= "\"time\": ".(0 + $payout->time).",";
					$json_payouts .= "\"amount\": \"{$payout->amount}\",";
					$json_payouts .= "\"tx\": \"{$payout->tx}\"";
					$json_payouts .= "}";
				}
				$json_payouts .= "]";
				controller()->memcache->set("api_payouts-".$user->id, $json_payouts, 60, MEMCACHE_COMPRESSED);
			}
			echo str_replace("},","},\n", $json_payouts);
		}

		echo "}";
	}

	/////////////////////////////////////////////////

	public function actionRental()
	{
		if(!LimitRequest('api-rental', 10)) return;
		if(!VEXON_RENTAL) return;

		$key = getparam('key');
		$renter = getdbosql('db_renters', "apikey=:apikey", array(':apikey'=>$key));
		if(!$renter) return;

		$balance = bitcoinvaluetoa($renter->balance);
		$unconfirmed = bitcoinvaluetoa($renter->unconfirmed);

		echo "{";
		echo "\"balance\": $balance, ";
		echo "\"unconfirmed\": $unconfirmed, ";

		echo "\"jobs\": [";
		$list = getdbolist('db_jobs', "renterid=$renter->id");
		foreach($list as $i=>$job)
		{
			if($i) echo ", ";

			$hashrate = vexon_job_rate($job->id);
			$hashrate_bad = vexon_job_rate_bad($job->id);

			echo '{';
			echo "\"jobid\": \"$job->id\", ";
			echo "\"algo\": \"$job->algo\", ";
			echo "\"price\": \"$job->price\", ";
			echo "\"hashrate\": \"$job->speed\", ";
			echo "\"server\": \"$job->host\", ";
			echo "\"port\": \"$job->port\", ";
			echo "\"username\": \"$job->username\", ";
			echo "\"password\": \"$job->password\", ";
			echo "\"started\": \"$job->ready\", ";
			echo "\"active\": \"$job->active\", ";
			echo "\"accepted\": \"$hashrate\", ";
			echo "\"rejected\": \"$hashrate_bad\", ";
			echo "\"diff\": \"$job->difficulty\"";

			echo '}';
		}

		echo "]}";
	}

	public function actionRental_price()
	{
		if(!VEXON_RENTAL) return;

		$key = getparam('key');
		$renter = getdbosql('db_renters', "apikey=:apikey", array(':apikey'=>$key));
		if(!$renter) return;

		$jobid = getparam('jobid');
		$price = getparam('price');

		$job = getdbo('db_jobs', $jobid);
		if($job->renterid != $renter->id) return;

		$job->price = $price;
		$job->time = time();
		$job->save();
	}

	public function actionRental_hashrate()
	{
		if(!VEXON_RENTAL) return;

		$key = getparam('key');
		$renter = getdbosql('db_renters', "apikey=:apikey", array(':apikey'=>$key));
		if(!$renter) return;

		$jobid = getparam('jobid');
		$hashrate = getparam('hashrate');

		$job = getdbo('db_jobs', $jobid);
		if($job->renterid != $renter->id) return;

		$job->speed = $hashrate;
		$job->time = time();
		$job->save();
	}

	public function actionRental_start()
	{
		if(!VEXON_RENTAL) return;

		$key = getparam('key');
		$renter = getdbosql('db_renters', "apikey=:apikey", array(':apikey'=>$key));
		if(!$renter || $renter->balance<=0) return;

		$jobid = getparam('jobid');

		$job = getdbo('db_jobs', $jobid);
		if($job->renterid != $renter->id) return;

		$job->ready = true;
		$job->time = time();
		$job->save();
	}

	public function actionRental_stop()
	{
		if(!VEXON_RENTAL) return;

		$key = getparam('key');
		$renter = getdbosql('db_renters', "apikey=:apikey", array(':apikey'=>$key));
		if(!$renter) return;

		$jobid = getparam('jobid');

		$job = getdbo('db_jobs', $jobid);
		if($job->renterid != $renter->id) return;

		$job->ready = false;
		$job->time = time();
		$job->save();
	}

}
