commit 1ebcd999a6455750c6f3c53409c81f9237a6acfa
Author: root
Date: Tue Jun 13 18:33:31 2017 +0000
Initial commit
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8d599e3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+/vendor/*
+/config/app.php
+/tmp/*
+/logs/*
+lbryexplorer.zip
+
diff --git a/.htaccess b/.htaccess
new file mode 100644
index 0000000..bc1dd9c
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,11 @@
+# Uncomment the following to prevent the httpoxy vulnerability
+# See: https://httpoxy.org/
+#
+# RequestHeader unset Proxy
+#
+
+
+ RewriteEngine on
+ RewriteRule ^$ webroot/ [L]
+ RewriteRule (.*) webroot/$1 [L]
+
diff --git a/bin/cake b/bin/cake
new file mode 100755
index 0000000..6801c45
--- /dev/null
+++ b/bin/cake
@@ -0,0 +1,46 @@
+#!/usr/bin/env sh
+################################################################################
+#
+# Cake is a shell script for invoking CakePHP shell commands
+#
+# CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+# Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+#
+# Licensed under The MIT License
+# For full copyright and license information, please see the LICENSE.txt
+# Redistributions of files must retain the above copyright notice.
+#
+# @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+# @link http://cakephp.org CakePHP(tm) Project
+# @since 1.2.0
+# @license http://www.opensource.org/licenses/mit-license.php MIT License
+#
+################################################################################
+
+# Canonicalize by following every symlink of the given name recursively
+canonicalize() {
+ NAME="$1"
+ if [ -f "$NAME" ]
+ then
+ DIR=$(dirname -- "$NAME")
+ NAME=$(cd -P "$DIR" > /dev/null && pwd -P)/$(basename -- "$NAME")
+ fi
+ while [ -h "$NAME" ]; do
+ DIR=$(dirname -- "$NAME")
+ SYM=$(readlink "$NAME")
+ NAME=$(cd "$DIR" > /dev/null && cd $(dirname -- "$SYM") > /dev/null && pwd)/$(basename -- "$SYM")
+ done
+ echo "$NAME"
+}
+
+CONSOLE=$(dirname -- "$(canonicalize "$0")")
+APP=$(dirname "$CONSOLE")
+
+if [ $(basename $0) != 'cake' ]
+then
+ exec php "$CONSOLE"/cake.php $(basename $0) "$@"
+else
+ exec php "$CONSOLE"/cake.php "$@"
+fi
+
+exit
diff --git a/bin/cake.bat b/bin/cake.bat
new file mode 100644
index 0000000..d63fa83
--- /dev/null
+++ b/bin/cake.bat
@@ -0,0 +1,27 @@
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::
+:: Cake is a Windows batch script for invoking CakePHP shell commands
+::
+:: CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+:: Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+::
+:: Licensed under The MIT License
+:: Redistributions of files must retain the above copyright notice.
+::
+:: @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+:: @link http://cakephp.org CakePHP(tm) Project
+:: @since 2.0.0
+:: @license http://www.opensource.org/licenses/mit-license.php MIT License
+::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+
+@echo off
+
+SET app=%0
+SET lib=%~dp0
+
+php "%lib%cake.php" %*
+
+echo.
+
+exit /B %ERRORLEVEL%
diff --git a/bin/cake.php b/bin/cake.php
new file mode 100644
index 0000000..5ee314c
--- /dev/null
+++ b/bin/cake.php
@@ -0,0 +1,34 @@
+#!/usr/bin/php -q
+require->php)) {
+ $minVersion = preg_replace('/([^0-9\.])/', '', $composer->require->php);
+ }
+}
+if (version_compare(phpversion(), $minVersion, '<')) {
+ fwrite(STDERR, sprintf("Minimum PHP version: %s. You are using: %s.\n", $minVersion, phpversion()));
+ exit(-1);
+}
+
+require dirname(__DIR__) . '/vendor/autoload.php';
+include dirname(__DIR__) . '/config/bootstrap.php';
+
+exit(Cake\Console\ShellDispatcher::run($argv));
diff --git a/config/app.default.php b/config/app.default.php
new file mode 100644
index 0000000..c06599e
--- /dev/null
+++ b/config/app.default.php
@@ -0,0 +1,346 @@
+ filter_var(env('DEBUG', true), FILTER_VALIDATE_BOOLEAN),
+
+ /**
+ * Configure basic information about the application.
+ *
+ * - namespace - The namespace to find app classes under.
+ * - defaultLocale - The default locale for translation, formatting currencies and numbers, date and time.
+ * - encoding - The encoding used for HTML + database connections.
+ * - base - The base directory the app resides in. If false this
+ * will be auto detected.
+ * - dir - Name of app directory.
+ * - webroot - The webroot directory.
+ * - wwwRoot - The file path to webroot.
+ * - baseUrl - To configure CakePHP to *not* use mod_rewrite and to
+ * use CakePHP pretty URLs, remove these .htaccess
+ * files:
+ * /.htaccess
+ * /webroot/.htaccess
+ * And uncomment the baseUrl key below.
+ * - fullBaseUrl - A base URL to use for absolute links.
+ * - imageBaseUrl - Web path to the public images directory under webroot.
+ * - cssBaseUrl - Web path to the public css directory under webroot.
+ * - jsBaseUrl - Web path to the public js directory under webroot.
+ * - paths - Configure paths for non class based resources. Supports the
+ * `plugins`, `templates`, `locales` subkeys, which allow the definition of
+ * paths for plugins, view templates and locale files respectively.
+ */
+ 'App' => [
+ 'namespace' => 'App',
+ 'encoding' => env('APP_ENCODING', 'UTF-8'),
+ 'defaultLocale' => env('APP_DEFAULT_LOCALE', 'en_US'),
+ 'base' => false,
+ 'dir' => 'src',
+ 'webroot' => 'webroot',
+ 'wwwRoot' => WWW_ROOT,
+ // 'baseUrl' => env('SCRIPT_NAME'),
+ 'fullBaseUrl' => false,
+ 'imageBaseUrl' => 'img/',
+ 'cssBaseUrl' => 'css/',
+ 'jsBaseUrl' => 'js/',
+ 'paths' => [
+ 'plugins' => [ROOT . DS . 'plugins' . DS],
+ 'templates' => [APP . 'Template' . DS],
+ 'locales' => [APP . 'Locale' . DS],
+ ],
+ ],
+
+ /**
+ * Security and encryption configuration
+ *
+ * - salt - A random string used in security hashing methods.
+ * The salt value is also used as the encryption key.
+ * You should treat it as extremely sensitive data.
+ */
+ 'Security' => [
+ 'salt' => env('SECURITY_SALT', '__SALT__'),
+ ],
+
+ /**
+ * Apply timestamps with the last modified time to static assets (js, css, images).
+ * Will append a querystring parameter containing the time the file was modified.
+ * This is useful for busting browser caches.
+ *
+ * Set to true to apply timestamps when debug is true. Set to 'force' to always
+ * enable timestamping regardless of debug value.
+ */
+ 'Asset' => [
+ // 'timestamp' => true,
+ ],
+
+ /**
+ * Configure the cache adapters.
+ */
+ 'Cache' => [
+ 'default' => [
+ 'className' => 'File',
+ 'path' => CACHE,
+ 'url' => env('CACHE_DEFAULT_URL', null),
+ ],
+
+ /**
+ * Configure the cache used for general framework caching.
+ * Translation cache files are stored with this configuration.
+ * Duration will be set to '+2 minutes' in bootstrap.php when debug = true
+ * If you set 'className' => 'Null' core cache will be disabled.
+ */
+ '_cake_core_' => [
+ 'className' => 'File',
+ 'prefix' => 'myapp_cake_core_',
+ 'path' => CACHE . 'persistent/',
+ 'serialize' => true,
+ 'duration' => '+1 years',
+ 'url' => env('CACHE_CAKECORE_URL', null),
+ ],
+
+ /**
+ * Configure the cache for model and datasource caches. This cache
+ * configuration is used to store schema descriptions, and table listings
+ * in connections.
+ * Duration will be set to '+2 minutes' in bootstrap.php when debug = true
+ */
+ '_cake_model_' => [
+ 'className' => 'File',
+ 'prefix' => 'myapp_cake_model_',
+ 'path' => CACHE . 'models/',
+ 'serialize' => true,
+ 'duration' => '+1 years',
+ 'url' => env('CACHE_CAKEMODEL_URL', null),
+ ],
+ ],
+
+ /**
+ * Configure the Error and Exception handlers used by your application.
+ *
+ * By default errors are displayed using Debugger, when debug is true and logged
+ * by Cake\Log\Log when debug is false.
+ *
+ * In CLI environments exceptions will be printed to stderr with a backtrace.
+ * In web environments an HTML page will be displayed for the exception.
+ * With debug true, framework errors like Missing Controller will be displayed.
+ * When debug is false, framework errors will be coerced into generic HTTP errors.
+ *
+ * Options:
+ *
+ * - `errorLevel` - int - The level of errors you are interested in capturing.
+ * - `trace` - boolean - Whether or not backtraces should be included in
+ * logged errors/exceptions.
+ * - `log` - boolean - Whether or not you want exceptions logged.
+ * - `exceptionRenderer` - string - The class responsible for rendering
+ * uncaught exceptions. If you choose a custom class you should place
+ * the file for that class in src/Error. This class needs to implement a
+ * render method.
+ * - `skipLog` - array - List of exceptions to skip for logging. Exceptions that
+ * extend one of the listed exceptions will also be skipped for logging.
+ * E.g.:
+ * `'skipLog' => ['Cake\Network\Exception\NotFoundException', 'Cake\Network\Exception\UnauthorizedException']`
+ * - `extraFatalErrorMemory` - int - The number of megabytes to increase
+ * the memory limit by when a fatal error is encountered. This allows
+ * breathing room to complete logging or error handling.
+ */
+ 'Error' => [
+ 'errorLevel' => E_ALL,
+ 'exceptionRenderer' => 'Cake\Error\ExceptionRenderer',
+ 'skipLog' => [],
+ 'log' => true,
+ 'trace' => true,
+ ],
+
+ /**
+ * Email configuration.
+ *
+ * By defining transports separately from delivery profiles you can easily
+ * re-use transport configuration across multiple profiles.
+ *
+ * You can specify multiple configurations for production, development and
+ * testing.
+ *
+ * Each transport needs a `className`. Valid options are as follows:
+ *
+ * Mail - Send using PHP mail function
+ * Smtp - Send using SMTP
+ * Debug - Do not send the email, just return the result
+ *
+ * You can add custom transports (or override existing transports) by adding the
+ * appropriate file to src/Mailer/Transport. Transports should be named
+ * 'YourTransport.php', where 'Your' is the name of the transport.
+ */
+ 'EmailTransport' => [
+ 'default' => [
+ 'className' => 'Mail',
+ // The following keys are used in SMTP transports
+ 'host' => 'localhost',
+ 'port' => 25,
+ 'timeout' => 30,
+ 'username' => 'user',
+ 'password' => 'secret',
+ 'client' => null,
+ 'tls' => null,
+ 'url' => env('EMAIL_TRANSPORT_DEFAULT_URL', null),
+ ],
+ ],
+
+ /**
+ * Email delivery profiles
+ *
+ * Delivery profiles allow you to predefine various properties about email
+ * messages from your application and give the settings a name. This saves
+ * duplication across your application and makes maintenance and development
+ * easier. Each profile accepts a number of keys. See `Cake\Mailer\Email`
+ * for more information.
+ */
+ 'Email' => [
+ 'default' => [
+ 'transport' => 'default',
+ 'from' => 'you@localhost',
+ //'charset' => 'utf-8',
+ //'headerCharset' => 'utf-8',
+ ],
+ ],
+
+ /**
+ * Connection information used by the ORM to connect
+ * to your application's datastores.
+ * Do not use periods in database name - it may lead to error.
+ * See https://github.com/cakephp/cakephp/issues/6471 for details.
+ * Drivers include Mysql Postgres Sqlite Sqlserver
+ * See vendor\cakephp\cakephp\src\Database\Driver for complete list
+ */
+ 'Datasources' => [
+ 'default' => [
+ 'className' => 'Cake\Database\Connection',
+ 'driver' => 'Cake\Database\Driver\Mysql',
+ 'persistent' => false,
+ 'host' => 'localhost',
+ /**
+ * CakePHP will use the default DB port based on the driver selected
+ * MySQL on MAMP uses port 8889, MAMP users will want to uncomment
+ * the following line and set the port accordingly
+ */
+ //'port' => 'non_standard_port_number',
+ 'username' => 'my_app',
+ 'password' => 'secret',
+ 'database' => 'my_app',
+ 'encoding' => 'utf8',
+ 'timezone' => 'UTC',
+ 'flags' => [],
+ 'cacheMetadata' => true,
+ 'log' => false,
+
+ /**
+ * Set identifier quoting to true if you are using reserved words or
+ * special characters in your table or column names. Enabling this
+ * setting will result in queries built using the Query Builder having
+ * identifiers quoted when creating SQL. It should be noted that this
+ * decreases performance because each query needs to be traversed and
+ * manipulated before being executed.
+ */
+ 'quoteIdentifiers' => false,
+
+ /**
+ * During development, if using MySQL < 5.6, uncommenting the
+ * following line could boost the speed at which schema metadata is
+ * fetched from the database. It can also be set directly with the
+ * mysql configuration directive 'innodb_stats_on_metadata = 0'
+ * which is the recommended value in production environments
+ */
+ //'init' => ['SET GLOBAL innodb_stats_on_metadata = 0'],
+
+ 'url' => env('DATABASE_URL', null),
+ ],
+
+ /**
+ * The test connection is used during the test suite.
+ */
+ 'test' => [
+ 'className' => 'Cake\Database\Connection',
+ 'driver' => 'Cake\Database\Driver\Mysql',
+ 'persistent' => false,
+ 'host' => 'localhost',
+ //'port' => 'non_standard_port_number',
+ 'username' => 'my_app',
+ 'password' => 'secret',
+ 'database' => 'test_myapp',
+ 'encoding' => 'utf8',
+ 'timezone' => 'UTC',
+ 'cacheMetadata' => true,
+ 'quoteIdentifiers' => false,
+ 'log' => false,
+ //'init' => ['SET GLOBAL innodb_stats_on_metadata = 0'],
+ 'url' => env('DATABASE_TEST_URL', null),
+ ],
+ ],
+
+ /**
+ * Configures logging options
+ */
+ 'Log' => [
+ 'debug' => [
+ 'className' => 'Cake\Log\Engine\FileLog',
+ 'path' => LOGS,
+ 'file' => 'debug',
+ 'levels' => ['notice', 'info', 'debug'],
+ 'url' => env('LOG_DEBUG_URL', null),
+ ],
+ 'error' => [
+ 'className' => 'Cake\Log\Engine\FileLog',
+ 'path' => LOGS,
+ 'file' => 'error',
+ 'levels' => ['warning', 'error', 'critical', 'alert', 'emergency'],
+ 'url' => env('LOG_ERROR_URL', null),
+ ],
+ ],
+
+ /**
+ * Session configuration.
+ *
+ * Contains an array of settings to use for session configuration. The
+ * `defaults` key is used to define a default preset to use for sessions, any
+ * settings declared here will override the settings of the default config.
+ *
+ * ## Options
+ *
+ * - `cookie` - The name of the cookie to use. Defaults to 'CAKEPHP'.
+ * - `cookiePath` - The url path for which session cookie is set. Maps to the
+ * `session.cookie_path` php.ini config. Defaults to base path of app.
+ * - `timeout` - The time in minutes the session should be valid for.
+ * Pass 0 to disable checking timeout.
+ * Please note that php.ini's session.gc_maxlifetime must be equal to or greater
+ * than the largest Session['timeout'] in all served websites for it to have the
+ * desired effect.
+ * - `defaults` - The default configuration set to use as a basis for your session.
+ * There are four built-in options: php, cake, cache, database.
+ * - `handler` - Can be used to enable a custom session handler. Expects an
+ * array with at least the `engine` key, being the name of the Session engine
+ * class to use for managing the session. CakePHP bundles the `CacheSession`
+ * and `DatabaseSession` engines.
+ * - `ini` - An associative array of additional ini values to set.
+ *
+ * The built-in `defaults` options are:
+ *
+ * - 'php' - Uses settings defined in your php.ini.
+ * - 'cake' - Saves session files in CakePHP's /tmp directory.
+ * - 'database' - Uses CakePHP's database sessions.
+ * - 'cache' - Use the Cache class to save sessions.
+ *
+ * To define a custom session handler, save it at src/Network/Session/.php.
+ * Make sure the class implements PHP's `SessionHandlerInterface` and set
+ * Session.handler to
+ *
+ * To use database sessions, load the SQL file located at config/Schema/sessions.sql
+ */
+ 'Session' => [
+ 'defaults' => 'php',
+ ],
+];
diff --git a/config/bootstrap.php b/config/bootstrap.php
new file mode 100644
index 0000000..a3ac0d0
--- /dev/null
+++ b/config/bootstrap.php
@@ -0,0 +1,222 @@
+getMessage() . "\n");
+}
+
+/*
+ * Load an environment local configuration file.
+ * You can use a file like app_local.php to provide local overrides to your
+ * shared configuration.
+ */
+//Configure::load('app_local', 'default');
+
+/*
+ * When debug = true the metadata cache should only last
+ * for a short time.
+ */
+if (Configure::read('debug')) {
+ Configure::write('Cache._cake_model_.duration', '+2 minutes');
+ Configure::write('Cache._cake_core_.duration', '+2 minutes');
+}
+
+/*
+ * Set server timezone to UTC. You can change it to another timezone of your
+ * choice but using UTC makes time calculations / conversions easier.
+ */
+date_default_timezone_set('UTC');
+
+/*
+ * Configure the mbstring extension to use the correct encoding.
+ */
+mb_internal_encoding(Configure::read('App.encoding'));
+
+/*
+ * Set the default locale. This controls how dates, number and currency is
+ * formatted and sets the default language to use for translations.
+ */
+ini_set('intl.default_locale', Configure::read('App.defaultLocale'));
+
+/*
+ * Register application error and exception handlers.
+ */
+$isCli = PHP_SAPI === 'cli';
+if ($isCli) {
+ (new ConsoleErrorHandler(Configure::read('Error')))->register();
+} else {
+ (new ErrorHandler(Configure::read('Error')))->register();
+}
+
+/*
+ * Include the CLI bootstrap overrides.
+ */
+if ($isCli) {
+ require __DIR__ . '/bootstrap_cli.php';
+}
+
+/*
+ * Set the full base URL.
+ * This URL is used as the base of all absolute links.
+ *
+ * If you define fullBaseUrl in your config file you can remove this.
+ */
+if (!Configure::read('App.fullBaseUrl')) {
+ $s = null;
+ if (env('HTTPS')) {
+ $s = 's';
+ }
+
+ $httpHost = env('HTTP_HOST');
+ if (isset($httpHost)) {
+ Configure::write('App.fullBaseUrl', 'http' . $s . '://' . $httpHost);
+ }
+ unset($httpHost, $s);
+}
+
+Cache::setConfig(Configure::consume('Cache'));
+ConnectionManager::setConfig(Configure::consume('Datasources'));
+Email::setConfigTransport(Configure::consume('EmailTransport'));
+Email::setConfig(Configure::consume('Email'));
+Log::setConfig(Configure::consume('Log'));
+Security::salt(Configure::consume('Security.salt'));
+
+/*
+ * The default crypto extension in 3.0 is OpenSSL.
+ * If you are migrating from 2.x uncomment this code to
+ * use a more compatible Mcrypt based implementation
+ */
+//Security::engine(new \Cake\Utility\Crypto\Mcrypt());
+
+/*
+ * Setup detectors for mobile and tablet.
+ */
+Request::addDetector('mobile', function ($request) {
+ $detector = new \Detection\MobileDetect();
+
+ return $detector->isMobile();
+});
+Request::addDetector('tablet', function ($request) {
+ $detector = new \Detection\MobileDetect();
+
+ return $detector->isTablet();
+});
+
+/*
+ * Enable immutable time objects in the ORM.
+ *
+ * You can enable default locale format parsing by adding calls
+ * to `useLocaleParser()`. This enables the automatic conversion of
+ * locale specific date formats. For details see
+ * @link http://book.cakephp.org/3.0/en/core-libraries/internationalization-and-localization.html#parsing-localized-datetime-data
+ */
+Type::build('time')
+ ->useImmutable();
+Type::build('date')
+ ->useImmutable();
+Type::build('datetime')
+ ->useImmutable();
+Type::build('timestamp')
+ ->useImmutable();
+
+/*
+ * Custom Inflector rules, can be set to correctly pluralize or singularize
+ * table, model, controller names or whatever other string is passed to the
+ * inflection functions.
+ */
+//Inflector::rules('plural', ['/^(inflect)or$/i' => '\1ables']);
+//Inflector::rules('irregular', ['red' => 'redlings']);
+//Inflector::rules('uninflected', ['dontinflectme']);
+//Inflector::rules('transliteration', ['/å/' => 'aa']);
+
+/*
+ * Plugins need to be loaded manually, you can either load them one by one or all of them in a single call
+ * Uncomment one of the lines below, as you need. make sure you read the documentation on Plugin to use more
+ * advanced ways of loading plugins
+ *
+ * Plugin::loadAll(); // Loads all plugins at once
+ * Plugin::load('Migrations'); //Loads a single plugin named Migrations
+ *
+ */
+
+/*
+ * Only try to load DebugKit in development mode
+ * Debug Kit should not be installed on a production system
+ */
+if (Configure::read('debug')) {
+ Plugin::load('DebugKit', ['bootstrap' => true]);
+}
diff --git a/config/bootstrap_cli.php b/config/bootstrap_cli.php
new file mode 100644
index 0000000..f822a55
--- /dev/null
+++ b/config/bootstrap_cli.php
@@ -0,0 +1,38 @@
+connect('/', ['controller' => 'Main', 'action' => 'index']);
+ $routes->connect('/address/*', ['controller' => 'Main', 'action' => 'address']);
+ $routes->connect('/blocks/*', ['controller' => 'Main', 'action' => 'blocks']);
+ $routes->connect('/find', ['controller' => 'Main', 'action' => 'find']);
+ $routes->connect('/realtime', ['controller' => 'Main', 'action' => 'realtime']);
+ $routes->connect('/tx/*', ['controller' => 'Main', 'action' => 'tx']);
+ $routes->connect('/qr/*', ['controller' => 'Main', 'action' => 'qr']);
+
+ $routes->connect('/api/v1/address/:addr/tag', ['controller' => 'Main', 'action' => 'apiaddrtag'], ['addr' => '[A-Za-z0-9,]+', 'pass' => ['addr']]);
+ $routes->connect('/api/v1/address/:addr/utxo', ['controller' => 'Main', 'action' => 'apiaddrutxo'], ['addr' => '[A-Za-z0-9,]+', 'pass' => ['addr']]);
+
+ $routes->connect('/api/v1/realtime/blocks', ['controller' => 'Main', 'action' => 'apirealtimeblocks']);
+ $routes->connect('/api/v1/realtime/tx', ['controller' => 'Main', 'action' => 'apirealtimetx']);
+ $routes->connect('/api/v1/recentblocks', ['controller' => 'Main', 'action' => 'apirecentblocks']);
+ $routes->connect('/api/v1/status', ['controller' => 'Main', 'action' => 'apistatus']);
+ //$routes->connect('/api/v1/recenttxs', ['controller' => 'Main', 'action' => 'apirecenttxs']);
+
+ //$routes->fallbacks(DashedRoute::class);
+});
+
+/**
+ * Load all plugin routes. See the Plugin documentation on
+ * how to customize the loading of plugin routes.
+ */
+Plugin::routes();
+
diff --git a/config/schema/i18n.sql b/config/schema/i18n.sql
new file mode 100644
index 0000000..47cf171
--- /dev/null
+++ b/config/schema/i18n.sql
@@ -0,0 +1,18 @@
+# Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+#
+# Licensed under The MIT License
+# For full copyright and license information, please see the LICENSE.txt
+# Redistributions of files must retain the above copyright notice.
+# MIT License (http://www.opensource.org/licenses/mit-license.php)
+
+CREATE TABLE i18n (
+ id int NOT NULL auto_increment,
+ locale varchar(6) NOT NULL,
+ model varchar(255) NOT NULL,
+ foreign_key int(10) NOT NULL,
+ field varchar(255) NOT NULL,
+ content text,
+ PRIMARY KEY (id),
+ UNIQUE INDEX I18N_LOCALE_FIELD(locale, model, foreign_key, field),
+ INDEX I18N_FIELD(model, foreign_key, field)
+);
diff --git a/config/schema/sessions.sql b/config/schema/sessions.sql
new file mode 100644
index 0000000..b5a5276
--- /dev/null
+++ b/config/schema/sessions.sql
@@ -0,0 +1,13 @@
+# Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+#
+# Licensed under The MIT License
+# For full copyright and license information, please see the LICENSE.txt
+# Redistributions of files must retain the above copyright notice.
+# MIT License (http://www.opensource.org/licenses/mit-license.php)
+
+CREATE TABLE sessions (
+ id char(40) NOT NULL,
+ data text,
+ expires INT(11) NOT NULL,
+ PRIMARY KEY (id)
+);
diff --git a/cron/addrtx.sh b/cron/addrtx.sh
new file mode 100755
index 0000000..b3c02e9
--- /dev/null
+++ b/cron/addrtx.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+cd /var/www/lbry.block.ng
+bin/cake block addrtxamounts
+
diff --git a/cron/blocks.sh b/cron/blocks.sh
new file mode 100755
index 0000000..dc5c788
--- /dev/null
+++ b/cron/blocks.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+cd /var/www/lbry.block.ng
+bin/cake block parsenewblocks
+
diff --git a/cron/blockstuff.php b/cron/blockstuff.php
new file mode 100644
index 0000000..6fa78cf
--- /dev/null
+++ b/cron/blockstuff.php
@@ -0,0 +1,134 @@
+_startHeight = $startBlock;
+ $this->_endHeight = $endBlock;
+ $this->_maxHeight = $maxHeight;
+ }
+
+ public function run() {
+ $conn = new \PDO("mysql:host=localhost;dbname=lbry", 'lbry-admin', '46D861aX#!yQ');
+ $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+ $data_error = false;
+ $conn->beginTransaction();
+ for ($curr_height = $this->_startHeight; $curr_height <= $this->_endHeight; $curr_height++) {
+ $idx_str = str_pad($curr_height, strlen($this->_maxHeight), '0', STR_PAD_LEFT);
+
+ // get the block hash
+ $req = ['method' => 'getblockhash', 'params' => [$curr_height]];
+ $response = BlockStuff::curl_json_post(BlockStuff::rpcurl, json_encode($req));
+ $json = json_decode($response);
+ $curr_block_hash = $json->result;
+
+ $req = ['method' => 'getblock', 'params' => [$curr_block_hash]];
+ $response = BlockStuff::curl_json_post(BlockStuff::rpcurl, json_encode($req));
+ $json = json_decode($response);
+ $curr_block = $json->result;
+
+ $stmt = $conn->prepare('UPDATE Blocks SET Confirmations = ? WHERE Height = ?');
+ try {
+ $stmt->execute([$curr_block->confirmations, $curr_height]);
+ echo "[$idx_str/$this->_maxHeight] Updated block height: $curr_height with confirmations $curr_block->confirmations.\n";
+ } catch (Exception $e) {
+ $data_error = true;
+ }
+ }
+
+ if ($data_error) {
+ echo "Rolling back changes.\n";
+ $conn->rollBack();
+ return;
+ }
+
+ echo "Committing data.\n";
+ $conn->commit();
+ }
+}
+
+class BlockStuff {
+ const rpcurl = 'http://lrpc:lrpc@127.0.0.1:9245';
+ public static function blocksync() {
+ self::lock('blocksync');
+
+ $conn = new \PDO("mysql:host=localhost;dbname=lbry", 'lbry-admin', '46D861aX#!yQ');
+
+ $stmt = $conn->prepare('SELECT Height FROM Blocks ORDER BY Height DESC LIMIT 1');
+ $stmt->execute([]);
+ $max_block = $stmt->fetch(PDO::FETCH_OBJ);
+ if ($max_block) {
+ $chunk_limit = 10;
+ $curr_height = 0;
+ $chunks = floor($max_block->Height / $chunk_limit);
+ $threads = [];
+ for ($i = 0; $i < $chunk_limit; $i++) {
+ $start = $curr_height;
+ $end = ($i == ($chunk_limit - 1)) ? $max_block->Height : $start + $chunks;
+ $curr_height += $chunks + 1;
+ $thread = new BlockSyncThread($start, $end, $max_block->Height);
+ $threads[] = $thread;
+
+ $thread->start();
+ }
+
+ for ($i = 0; $i < count($threads); $i++) {
+ $threads[$i]->join();
+ }
+ }
+
+ self::unlock('blocksync');
+ }
+
+ public static function curl_json_post($url, $data, $headers = []) {
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+
+ $response = curl_exec($ch);
+ //Log::debug('Request execution completed.');
+ if ($response === false) {
+ $error = curl_error($ch);
+ $errno = curl_errno($ch);
+ curl_close($ch);
+
+ throw new \Exception(sprintf('The request failed: %s', $error), $errno);
+ } else {
+ curl_close($ch);
+ }
+
+ // Close any open file handle
+ return $response;
+ }
+
+ public static function lock($process_name) {
+ if (!is_dir(TMP . 'lock')) {
+ mkdir(TMP . 'lock');
+ }
+ $lock_file = TMP . 'lock' . DS . $process_name;
+ if (file_exists($lock_file)) {
+ echo "$process_name is already running.\n";
+ exit(0);
+ }
+ file_put_contents($lock_file, '1');
+ }
+
+ public static function unlock($process_name) {
+ $lock_file = TMP . 'lock' . DS . $process_name;
+ if (file_exists($lock_file)) {
+ unlink($lock_file);
+ }
+ return true;
+ }
+}
+
+BlockStuff::blocksync();
diff --git a/cron/blocktx.sh b/cron/blocktx.sh
new file mode 100755
index 0000000..0cead96
--- /dev/null
+++ b/cron/blocktx.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+cd /var/www/lbry.block.ng
+bin/cake block parsetxs
+
diff --git a/cron/confirmations.sh b/cron/confirmations.sh
new file mode 100755
index 0000000..585160a
--- /dev/null
+++ b/cron/confirmations.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+cd /var/www/lbry.block.ng/cron
+php blockstuff.php
diff --git a/cron/fixzero.sh b/cron/fixzero.sh
new file mode 100755
index 0000000..039fea1
--- /dev/null
+++ b/cron/fixzero.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+cd /var/www/lbry.block.ng
+bin/cake block fixzerooutputs
+
diff --git a/cron/forevermempool.sh b/cron/forevermempool.sh
new file mode 100755
index 0000000..2eab3d7
--- /dev/null
+++ b/cron/forevermempool.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+cd /var/www/lbry.block.ng
+bin/cake block forevermempool &
+
diff --git a/cron/livetx.sh b/cron/livetx.sh
new file mode 100755
index 0000000..18ac6bc
--- /dev/null
+++ b/cron/livetx.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+cd /var/www/lbry.block.ng
+bin/cake block parsemempool
+
diff --git a/cron/spends.sh b/cron/spends.sh
new file mode 100755
index 0000000..c5f5402
--- /dev/null
+++ b/cron/spends.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+cd /var/www/lbry.block.ng
+bin/cake block updatespends
+
diff --git a/cron/verifytags.sh b/cron/verifytags.sh
new file mode 100755
index 0000000..653c468
--- /dev/null
+++ b/cron/verifytags.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+cd /var/www/lbry.block.ng
+bin/cake aux verifytags
+
diff --git a/index.php b/index.php
new file mode 100644
index 0000000..fc5e39c
--- /dev/null
+++ b/index.php
@@ -0,0 +1,16 @@
+add(ErrorHandlerMiddleware::class)
+
+ // Handle plugin/theme assets like CakePHP normally does.
+ ->add(AssetMiddleware::class)
+
+ // Apply routing
+ ->add(RoutingMiddleware::class);
+
+ return $middleware;
+ }
+}
diff --git a/src/Console/Installer.php b/src/Console/Installer.php
new file mode 100644
index 0000000..0756096
--- /dev/null
+++ b/src/Console/Installer.php
@@ -0,0 +1,195 @@
+getIO();
+
+ $rootDir = dirname(dirname(__DIR__));
+
+ static::createAppConfig($rootDir, $io);
+ static::createWritableDirectories($rootDir, $io);
+
+ // ask if the permissions should be changed
+ if ($io->isInteractive()) {
+ $validator = function ($arg) {
+ if (in_array($arg, ['Y', 'y', 'N', 'n'])) {
+ return $arg;
+ }
+ throw new Exception('This is not a valid answer. Please choose Y or n.');
+ };
+ $setFolderPermissions = $io->askAndValidate(
+ 'Set Folder Permissions ? (Default to Y) [Y,n ]? ',
+ $validator,
+ 10,
+ 'Y'
+ );
+
+ if (in_array($setFolderPermissions, ['Y', 'y'])) {
+ static::setFolderPermissions($rootDir, $io);
+ }
+ } else {
+ static::setFolderPermissions($rootDir, $io);
+ }
+
+ static::setSecuritySalt($rootDir, $io);
+
+ if (class_exists('\Cake\Codeception\Console\Installer')) {
+ \Cake\Codeception\Console\Installer::customizeCodeceptionBinary($event);
+ }
+ }
+
+ /**
+ * Create the config/app.php file if it does not exist.
+ *
+ * @param string $dir The application's root directory.
+ * @param \Composer\IO\IOInterface $io IO interface to write to console.
+ * @return void
+ */
+ public static function createAppConfig($dir, $io)
+ {
+ $appConfig = $dir . '/config/app.php';
+ $defaultConfig = $dir . '/config/app.default.php';
+ if (!file_exists($appConfig)) {
+ copy($defaultConfig, $appConfig);
+ $io->write('Created `config/app.php` file');
+ }
+ }
+
+ /**
+ * Create the `logs` and `tmp` directories.
+ *
+ * @param string $dir The application's root directory.
+ * @param \Composer\IO\IOInterface $io IO interface to write to console.
+ * @return void
+ */
+ public static function createWritableDirectories($dir, $io)
+ {
+ $paths = [
+ 'logs',
+ 'tmp',
+ 'tmp/cache',
+ 'tmp/cache/models',
+ 'tmp/cache/persistent',
+ 'tmp/cache/views',
+ 'tmp/sessions',
+ 'tmp/tests'
+ ];
+
+ foreach ($paths as $path) {
+ $path = $dir . '/' . $path;
+ if (!file_exists($path)) {
+ mkdir($path);
+ $io->write('Created `' . $path . '` directory');
+ }
+ }
+ }
+
+ /**
+ * Set globally writable permissions on the "tmp" and "logs" directory.
+ *
+ * This is not the most secure default, but it gets people up and running quickly.
+ *
+ * @param string $dir The application's root directory.
+ * @param \Composer\IO\IOInterface $io IO interface to write to console.
+ * @return void
+ */
+ public static function setFolderPermissions($dir, $io)
+ {
+ // Change the permissions on a path and output the results.
+ $changePerms = function ($path, $perms, $io) {
+ // Get permission bits from stat(2) result.
+ $currentPerms = fileperms($path) & 0777;
+ if (($currentPerms & $perms) == $perms) {
+ return;
+ }
+
+ $res = chmod($path, $currentPerms | $perms);
+ if ($res) {
+ $io->write('Permissions set on ' . $path);
+ } else {
+ $io->write('Failed to set permissions on ' . $path);
+ }
+ };
+
+ $walker = function ($dir, $perms, $io) use (&$walker, $changePerms) {
+ $files = array_diff(scandir($dir), ['.', '..']);
+ foreach ($files as $file) {
+ $path = $dir . '/' . $file;
+
+ if (!is_dir($path)) {
+ continue;
+ }
+
+ $changePerms($path, $perms, $io);
+ $walker($path, $perms, $io);
+ }
+ };
+
+ $worldWritable = bindec('0000000111');
+ $walker($dir . '/tmp', $worldWritable, $io);
+ $changePerms($dir . '/tmp', $worldWritable, $io);
+ $changePerms($dir . '/logs', $worldWritable, $io);
+ }
+
+ /**
+ * Set the security.salt value in the application's config file.
+ *
+ * @param string $dir The application's root directory.
+ * @param \Composer\IO\IOInterface $io IO interface to write to console.
+ * @return void
+ */
+ public static function setSecuritySalt($dir, $io)
+ {
+ $config = $dir . '/config/app.php';
+ $content = file_get_contents($config);
+
+ $newKey = hash('sha256', Security::randomBytes(64));
+ $content = str_replace('__SALT__', $newKey, $content, $count);
+
+ if ($count == 0) {
+ $io->write('No Security.salt placeholder to replace.');
+
+ return;
+ }
+
+ $result = file_put_contents($config, $content);
+ if ($result) {
+ $io->write('Updated Security.salt value in config/app.php');
+
+ return;
+ }
+ $io->write('Unable to update Security.salt value.');
+ }
+}
diff --git a/src/Controller/AppController.php b/src/Controller/AppController.php
new file mode 100644
index 0000000..afd65cd
--- /dev/null
+++ b/src/Controller/AppController.php
@@ -0,0 +1,69 @@
+loadComponent('Security');`
+ *
+ * @return void
+ */
+ public function initialize()
+ {
+ parent::initialize();
+
+ $this->loadComponent('RequestHandler');
+ $this->loadComponent('Flash');
+
+ /*
+ * Enable the following components for recommended CakePHP security settings.
+ * see http://book.cakephp.org/3.0/en/controllers/components/security.html
+ */
+ //$this->loadComponent('Security');
+ //$this->loadComponent('Csrf');
+ }
+
+ /**
+ * Before render callback.
+ *
+ * @param \Cake\Event\Event $event The beforeRender event.
+ * @return \Cake\Network\Response|null|void
+ */
+ public function beforeRender(Event $event)
+ {
+ if (!array_key_exists('_serialize', $this->viewVars) &&
+ in_array($this->response->type(), ['application/json', 'application/xml'])
+ ) {
+ $this->set('_serialize', true);
+ }
+ }
+}
diff --git a/src/Controller/ErrorController.php b/src/Controller/ErrorController.php
new file mode 100644
index 0000000..cf0ebd5
--- /dev/null
+++ b/src/Controller/ErrorController.php
@@ -0,0 +1,68 @@
+loadComponent('RequestHandler');
+ }
+
+ /**
+ * beforeFilter callback.
+ *
+ * @param \Cake\Event\Event $event Event.
+ * @return \Cake\Network\Response|null|void
+ */
+ public function beforeFilter(Event $event)
+ {
+ }
+
+ /**
+ * beforeRender callback.
+ *
+ * @param \Cake\Event\Event $event Event.
+ * @return \Cake\Network\Response|null|void
+ */
+ public function beforeRender(Event $event)
+ {
+ parent::beforeRender($event);
+
+ $this->viewBuilder()->setTemplatePath('Error');
+ }
+
+ /**
+ * afterFilter callback.
+ *
+ * @param \Cake\Event\Event $event Event.
+ * @return \Cake\Network\Response|null|void
+ */
+ public function afterFilter(Event $event)
+ {
+ }
+}
diff --git a/src/Controller/MainController.php b/src/Controller/MainController.php
new file mode 100644
index 0000000..3c73149
--- /dev/null
+++ b/src/Controller/MainController.php
@@ -0,0 +1,617 @@
+redis = new \Predis\Client('tcp://127.0.0.1:6379');
+ }
+
+ protected function _getLatestPrice() {
+ $now = new \DateTime('now', new \DateTimeZone('UTC'));
+ $priceInfo = new \stdClass();
+ $priceInfo->time = $now->format('c');
+
+ $shouldRefreshPrice = false;
+ if (!$this->redis->exists(self::lbcPriceKey)) {
+ $shouldRefreshPrice = true;
+ } else {
+ $priceInfo = json_decode($this->redis->get(self::lbcPriceKey));
+ $lastPriceDt = new \DateTime($priceInfo->time);
+ $diff = $now->diff($lastPriceDt);
+ $diffHours = $diff->h;
+ $diffHours = $diffHours + ($diff->days * 24);
+ if ($diffHours >= 3) {
+ $shouldRefreshPrice = true;
+ }
+ }
+
+ if ($shouldRefreshPrice) {
+ $btrxjson = json_decode(self::curl_get(self::bittrexMarketUrl));
+ $blckjson = json_decode(self::curl_get(self::blockchainTickerUrl));
+
+ if ($btrxjson->success) {
+ $onelbc = $btrxjson->result->Ask;
+ $lbcPrice = 0;
+ if (isset($blckjson->USD)) {
+ $lbcPrice = $onelbc * $blckjson->USD->buy;
+ $priceInfo->price = number_format($lbcPrice, 2, '.', '');
+ $priceInfo->time = $now->format('c');
+ $this->redis->set(self::lbcPriceKey, json_encode($priceInfo));
+ }
+ }
+ }
+
+ $lbcUsdPrice = isset($priceInfo->price) ? '$' . $priceInfo->price : 'N/A';
+ return $lbcUsdPrice;
+ }
+
+ public function index() {
+ $this->loadModel('Blocks');
+
+ $lbcUsdPrice = $this->_getLatestPrice();
+ $this->set('lbcUsdPrice', $lbcUsdPrice);
+
+ $blocks = $this->Blocks->find()->select(['Chainwork', 'Confirmations', 'Difficulty', 'Hash', 'Height', 'TransactionHashes', 'BlockTime', 'BlockSize'])->
+ order(['Height' => 'desc'])->limit(6)->toArray();
+ for ($i = 0; $i < count($blocks); $i++) {
+ $tx_hashes = json_decode($blocks[$i]->TransactionHashes);
+ $blocks[$i]->TransactionCount = count($tx_hashes);
+ }
+
+ // try to calculate the hashrate based on the last 12 blocks found
+ $diffBlocks = $this->Blocks->find()->select(['Chainwork', 'BlockTime', 'Difficulty'])->order(['Height' => 'desc'])->limit(12)->toArray();
+ $hashRate = 'N/A';
+ if (count($diffBlocks) > 1) {
+ $highestBlock = $diffBlocks[0];
+ $lowestBlock = $diffBlocks[count($diffBlocks) - 1];
+
+ $maxTime = max($highestBlock->BlockTime, $lowestBlock->BlockTime);
+ $minTime = min($highestBlock->BlockTime, $lowestBlock->BlockTime);
+ $timeDiff = $maxTime - $minTime;
+ $math = EccFactory::getAdapter();
+ $workDiff = bcsub($math->hexDec($highestBlock->Chainwork), $math->hexDec($lowestBlock->Chainwork));
+ if ($timeDiff > 0) {
+ $hashRate = $this->_formatHashRate(bcdiv($workDiff, $timeDiff)) . '/s';
+ }
+ }
+
+ $this->set('recentBlocks', $blocks);
+ $this->set('hashRate', $hashRate);
+ }
+
+ public function realtime() {
+ $this->loadModel('Blocks');
+ $this->loadModel('Transactions');
+
+ // load 10 blocks and transactions
+ $conn = ConnectionManager::get('default');
+ $blocks = $this->Blocks->find()->select(['Height', 'BlockTime', 'TransactionHashes'])->order(['Height' => 'desc'])->limit(10)->toArray();
+ for ($i = 0; $i < count($blocks); $i++) {
+ $tx_hashes = json_decode($blocks[$i]->TransactionHashes);
+ $blocks[$i]->TransactionCount = count($tx_hashes);
+ }
+
+ $stmt = $conn->execute('SELECT T.Hash, T.InputCount, T.OutputCount, T.Value, IFNULL(T.TransactionTime, T.CreatedTime) AS TxTime ' .
+ 'FROM Transactions T ORDER BY CreatedTime DESC LIMIT 10');
+ $txs = $stmt->fetchAll(\PDO::FETCH_OBJ);
+
+ $this->set('blocks', $blocks);
+ $this->set('txs', $txs);
+ }
+
+ public function apirealtimeblocks() {
+ // load 10 blocks
+ $this->autoRender = false;
+ $this->loadModel('Blocks');
+ $blocks = $this->Blocks->find()->select(['Height', 'BlockTime', 'TransactionHashes'])->order(['Height' => 'desc'])->limit(10)->toArray();
+ for ($i = 0; $i < count($blocks); $i++) {
+ $tx_hashes = json_decode($blocks[$i]->TransactionHashes);
+ $blocks[$i]->TransactionCount = count($tx_hashes);
+ unset($blocks[$i]->TransactionHashes);
+ }
+
+ $this->_jsonResponse(['success' => true, 'blocks' => $blocks]);
+ }
+
+ public function apirealtimetx() {
+ // load 10 transactions
+ $this->autoRender = false;
+ $conn = ConnectionManager::get('default');
+ $stmt = $conn->execute('SELECT T.Hash, T.InputCount, T.OutputCount, T.Value, IFNULL(T.TransactionTime, T.CreatedTime) AS TxTime ' .
+ 'FROM Transactions T ORDER BY CreatedTime DESC LIMIT 10');
+ $txs = $stmt->fetchAll(\PDO::FETCH_OBJ);
+
+ $this->_jsonResponse(['success' => true, 'txs' => $txs]);
+ }
+
+ protected function _formatHashRate($value) {
+ /*if ($value > 1000000000000) {
+ return number_format( $value / 1000000000000, 2, '.', '' ) . ' TH';
+ }*/
+ if ($value > 1000000000) {
+ return number_format( $value / 1000000000, 2, '.', '' ) . ' GH';
+ }
+ if ($value > 1000000) {
+ return number_format( $value / 1000000, 2, '.', '' ) . ' MH';
+ }
+ if ($value > 1000) {
+ return number_format( $value / 1000, 2, '.', '' ) . ' KH';
+ }
+
+ return number_format($value) . ' H';
+ }
+
+ public function find() {
+ $criteria = $this->request->query('q');
+ if ($criteria === null || strlen(trim($criteria)) == 0) {
+ return $this->redirect('/');
+ }
+
+ $this->loadModel('Blocks');
+ $this->loadModel('Addresses');
+ $this->loadModel('Transactions');
+
+ if (is_numeric($criteria)) {
+ $height = (int) $criteria;
+ $block = $this->Blocks->find()->select(['Id'])->where(['Height' => $height])->first();
+ if ($block) {
+ return $this->redirect('/blocks/' . $height);
+ }
+ } else if (strlen(trim($criteria)) <= 40) {
+ // Address
+ $address = $this->Addresses->find()->select(['Id', 'Address'])->where(['Address' => $criteria])->first();
+ if ($address) {
+ return $this->redirect('/address/' . $address->Address);
+ }
+ } else {
+ // Try block hash first
+ $block = $this->Blocks->find()->select(['Height'])->where(['Hash' => $criteria])->first();
+ if ($block) {
+ return $this->redirect('/blocks/' . $block->Height);
+ } else {
+ $tx = $this->Transactions->find()->select(['Hash'])->where(['Hash' => $criteria])->first();
+ if ($tx) {
+ return $this->redirect('/tx/' . $tx->Hash);
+ }
+ }
+ }
+
+ // Not found, redirect to index
+ return $this->redirect('/');
+ }
+
+ public function blocks($height = null) {
+ $this->loadModel('Blocks');
+
+ if ($height === null) {
+ // paginate blocks
+ return $this->redirect('/');
+ } else {
+ $this->loadModel('Transactions');
+ $height = intval($height);
+ if ($height < 0) {
+ return $this->redirect('/');
+ }
+
+ $block = $this->Blocks->find()->where(['Height' => $height])->first();
+ if (!$block) {
+ return $this->redirect('/');
+ }
+
+ try {
+ // update the block confirmations
+ $req = ['method' => 'getblock', 'params' => [$block->Hash]];
+ $response = self::curl_json_post(self::rpcurl, json_encode($req));
+ $json = json_decode($response);
+ $rpc_block = $json->result;
+ if (isset($rpc_block->confirmations)) {
+ $block->Confirmations = $rpc_block->confirmations;
+ $conn = ConnectionManager::get('default');
+ $conn->execute('UPDATE Blocks SET Confirmations = ? WHERE Id = ?', [$rpc_block->confirmations, $block->Id]);
+ }
+ } catch (\Exception $e) {
+ // try again next time
+ }
+
+ // Get the basic block transaction info
+ $txs = $this->Transactions->find()->select(['InputCount', 'OutputCount', 'Hash', 'Value', 'Version'])->where(['BlockHash' => $block->Hash])->toArray();
+
+ $this->set('block', $block);
+ $this->set('blockTxs', $txs);
+ }
+ }
+
+ public function tx($hash = null) {
+ $this->loadModel('Blocks');
+ $this->loadModel('Transactions');
+ $this->loadModel('Inputs');
+ $this->loadModel('Outputs');
+ $sourceAddress = $this->request->query('address');
+
+ if (!$hash) {
+ return $this->redirect('/');
+ }
+
+ $tx = $this->Transactions->find()->select(
+ ['Id', 'BlockHash', 'InputCount', 'OutputCount', 'Hash', 'Value', 'TransactionTime', 'TransactionSize', 'Created', 'Version', 'LockTime', 'Raw'])->where(['Hash' => $hash])->first();
+ if (!$tx) {
+ return $this->redirect('/');
+ }
+
+ if ($tx->TransactionSize == 0) {
+ $tx->TransactionSize = (strlen($tx->Raw) / 2);
+ $conn = ConnectionManager::get('default');
+ $conn->execute('UPDATE Transactions SET TransactionSize = ? WHERE Id = ?', [$tx->TransactionSize, $tx->Id]);
+ }
+
+ $block = $this->Blocks->find()->select(['Confirmations', 'Height'])->where(['Hash' => $tx->BlockHash])->first();
+ $inputs = $this->Inputs->find()->contain(['InputAddresses'])->where(['TransactionId' => $tx->Id])->order(['PrevoutN' => 'asc'])->toArray();
+ $outputs = $this->Outputs->find()->contain(['OutputAddresses', 'SpendInput' => ['fields' => ['Id', 'TransactionHash', 'PrevoutN', 'PrevoutHash']]])->where(['Outputs.TransactionId' => $tx->Id])->order(['Vout' => 'asc'])->toArray();
+ for ($i = 0; $i < count($outputs); $i++) {
+ $outputs[$i]->IsClaim = (strpos($outputs[$i]->ScriptPubKeyAsm, 'CLAIM') > -1);
+ $outputs[$i]->IsSupportClaim = (strpos($outputs[$i]->ScriptPubKeyAsm, 'SUPPORT_CLAIM') > -1);
+ $outputs[$i]->IsUpdateClaim = (strpos($outputs[$i]->ScriptPubKeyAsm, 'UPDATE_CLAIM') > -1);
+ }
+
+ $totalIn = 0;
+ $totalOut = 0;
+ $fee = 0;
+ foreach ($inputs as $in) {
+ $totalIn = bcadd($totalIn, $in->Value, 8);
+ }
+ foreach ($outputs as $out) {
+ $totalOut = bcadd($totalOut, $out->Value, 8);
+ }
+ $fee = bcsub($totalIn, $totalOut, 8);
+
+ $this->set('tx', $tx);
+ $this->set('block', $block);
+ $this->set('confirmations', $block ? number_format($block->Confirmations, 0, '', ',') : '0');
+ $this->set('fee', $fee);
+ $this->set('inputs', $inputs);
+ $this->set('outputs', $outputs);
+ $this->set('sourceAddress', $sourceAddress);
+ }
+
+ public function address($addr = null) {
+ set_time_limit(0);
+
+ $this->loadModel('Addresses');
+ $this->loadModel('Transactions');
+ $this->loadModel('Inputs');
+ $this->loadModel('Outputs');
+
+ if (!$addr) {
+ return $this->redirect('/');
+ }
+
+ $canTag = false;
+ $totalRecvAmount = 0;
+ $totalSentAmount = 0;
+ $balanceAmount = 0;
+ $recentTxs = [];
+
+ $tagRequestAmount = 0;
+ // Check for pending tag request
+ $this->loadModel('TagAddressRequests');
+ $pending = $this->TagAddressRequests->find()->where(['Address' => $addr, 'IsVerified <>' => 1])->first();
+ if (!$pending) {
+ $tagRequestAmount = '25.' . rand(11111111, 99999999);
+ }
+
+ $address = $this->Addresses->find()->where(['Address' => $addr])->first();
+ if (!$address) {
+ if (strlen($addr) === 34) {
+ $address = new \stdClass();
+ $address->Address = $addr;
+ } else {
+ return $this->redirect('/');
+ }
+ } else {
+ $conn = ConnectionManager::get('default');
+
+ $canTag = true;
+ $addressId = $address->Id;
+
+ $stmt = $conn->execute('SELECT A.TotalReceived, A.TotalSent FROM Addresses A WHERE A.Id = ?', [$address->Id]);
+ $totals = $stmt->fetch(\PDO::FETCH_OBJ);
+
+ $stmt = $conn->execute('SELECT T.Id, T.Hash, T.InputCount, T.OutputCount, T.Value, ' .
+ 'TA.DebitAmount, TA.CreditAmount, ' .
+ 'B.Height, B.Confirmations, IFNULL(T.TransactionTime, T.CreatedTime) AS TxTime ' .
+ 'FROM Transactions T ' .
+ 'LEFT JOIN Blocks B ON T.BlockHash = B.Hash ' .
+ 'RIGHT JOIN (SELECT TransactionId, DebitAmount, CreditAmount FROM TransactionsAddresses ' .
+ ' WHERE AddressId = ? ORDER BY TransactionTime DESC LIMIT 0, 20) TA ON TA.TransactionId = T.Id', [$addressId]);
+ $recentTxs = $stmt->fetchAll(\PDO::FETCH_OBJ);
+
+ $totalRecvAmount = $totals->TotalReceived == 0 ? '0' : $totals->TotalReceived + 0;
+ $totalSentAmount = $totals->TotalSent == 0 ? '0' : $totals->TotalSent + 0;
+ $balanceAmount = bcsub($totalRecvAmount, $totalSentAmount, 8) + 0;
+ }
+
+ $this->set('canTag', $canTag);
+ $this->set('pending', $pending);
+ $this->set('tagRequestAmount', $tagRequestAmount);
+ $this->set('address', $address);
+ $this->set('totalReceived', $totalRecvAmount);
+ $this->set('totalSent', $totalSentAmount);
+ $this->set('balanceAmount', $balanceAmount);
+ $this->set('recentTxs', $recentTxs);
+ }
+
+ public function qr($data = null) {
+ $this->autoRender = false;
+
+ if (!$data || strlen(trim($data)) == 0 || strlen(trim($data)) > 50) {
+ return;
+ }
+
+ $qrCode = new QrCode($data);
+ $qrCode->setSize(300);
+
+ // Set advanced options
+ $qrCode
+ ->setWriterByName('png')
+ ->setMargin(10)
+ ->setEncoding('UTF-8')
+ ->setErrorCorrectionLevel(ErrorCorrectionLevel::LOW)
+ ->setForegroundColor(['r' => 0, 'g' => 0, 'b' => 0])
+ ->setBackgroundColor(['r' => 255, 'g' => 255, 'b' => 255])
+ ->setLogoWidth(150)
+ ->setValidateResult(false);
+
+ header('Content-Type: '.$qrCode->getContentType());
+ echo $qrCode->writeString();
+ exit(0);
+ }
+
+ public static function curl_get($url) {
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+
+ $response = curl_exec($ch);
+ Log::debug('Request execution completed.');
+
+ if ($response === false) {
+ $error = curl_error($ch);
+ $errno = curl_errno($ch);
+ curl_close($ch);
+
+ throw new \Exception(sprintf('The request failed: %s', $error), $errno);
+ } else {
+ curl_close($ch);
+ }
+
+ return $response;
+ }
+
+ public function apistatus() {
+ $this->autoRender = false;
+ $this->loadModel('Blocks');
+
+ // Get the max height block
+ $height = 0;
+ $difficulty = 0;
+ $highestBlock = $this->Blocks->find()->select(['Height', 'Difficulty'])->order(['Height' => 'desc'])->first();
+ $height = $highestBlock->Height;
+ $difficulty = $highestBlock->Difficulty;
+ $lbcUsdPrice = $this->_getLatestPrice();
+
+ // Calculate hash rate
+ $diffBlocks = $this->Blocks->find()->select(['Chainwork', 'BlockTime', 'Difficulty'])->order(['Height' => 'desc'])->limit(12)->toArray();
+ $hashRate = 'N/A';
+ if (count($diffBlocks) > 1) {
+ $highestBlock = $diffBlocks[0];
+ $lowestBlock = $diffBlocks[count($diffBlocks) - 1];
+
+ $maxTime = max($highestBlock->BlockTime, $lowestBlock->BlockTime);
+ $minTime = min($highestBlock->BlockTime, $lowestBlock->BlockTime);
+ $timeDiff = $maxTime - $minTime;
+ $math = EccFactory::getAdapter();
+ $workDiff = bcsub($math->hexDec($highestBlock->Chainwork), $math->hexDec($lowestBlock->Chainwork));
+ if ($timeDiff > 0) {
+ $hashRate = $this->_formatHashRate(bcdiv($workDiff, $timeDiff)) . '/s';
+ }
+ }
+
+ return $this->_jsonResponse(['success' => true, 'status' => [
+ 'height' => $height,
+ 'difficulty' => number_format($difficulty, 2, '.', ''),
+ 'price' => $lbcUsdPrice,
+ 'hashrate' => $hashRate
+ ]]);
+ }
+
+ public function apirecentblocks() {
+ $this->autoRender = false;
+ $this->loadModel('Blocks');
+ $blocks = $this->Blocks->find()->select(['Difficulty', 'Hash', 'Height', 'TransactionHashes', 'BlockTime', 'BlockSize'])->
+ order(['Height' => 'desc'])->limit(6)->toArray();
+ for ($i = 0; $i < count($blocks); $i++) {
+ $tx_hashes = json_decode($blocks[$i]->TransactionHashes);
+ $blocks[$i]->TransactionCount = count($tx_hashes);
+ $blocks[$i]->Difficulty = number_format($blocks[$i]->Difficulty, 2, '.', '');
+ unset($blocks[$i]->TransactionHashes);
+ }
+ return $this->_jsonResponse(['success' => true, 'blocks' => $blocks]);
+ }
+
+ public function apiaddrtag($base58address = null) {
+ $this->autoRender = false;
+ if (!isset($base58address) || strlen(trim($base58address)) !== 34) {
+ return $this->_jsonError('Invalid base58 address not specified.', 400);
+ }
+ if (!$this->request->is('post')) {
+ return $this->_jsonError('Invalid HTTP request method.', 400);
+ }
+
+ if (trim($base58address) == self::tagReceiptAddress) {
+ return $this->_jsonError('You cannot submit a tag request for this address.', 400);
+ }
+
+ $this->loadModel('Addresses');
+ $this->loadModel('TagAddressRequests');
+ $data = [
+ 'Address' => $base58address,
+ 'Tag' => trim($this->request->data('tag')),
+ 'TagUrl' => trim($this->request->data('url')),
+ 'VerificationAmount' => $this->request->data('vamount')
+ ];
+
+ // verify
+ $entity = $this->TagAddressRequests->newEntity($data);
+ if (strlen($entity->Tag) === 0 || strlen($entity->Tag) > 30) {
+ return $this->_jsonError('Oops! Please specify a valid tag. It should be no more than 30 characters long.', 400);
+ }
+
+ if (strlen($entity->TagUrl) > 0) {
+ if (strlen($entity->TagUrl) > 200) {
+ return $this->_jsonError('Oops! The link should be no more than 200 characters long.', 400);
+ }
+ if (!filter_var($entity->TagUrl, FILTER_VALIDATE_URL)) {
+ return $this->_jsonError('Oops! The link should be a valid URL.', 400);
+ }
+ } else {
+ unset($entity->TagUrl);
+ }
+
+ if ($entity->VerificationAmount < 25.1 || $entity->VerificationAmount > 25.99999999) {
+ return $this->_jsonError('Oops! The verification amount is invalid. Please refresh the page and try again.', 400);
+ }
+
+ // check if the tag is taken
+ $addrTag = $this->Addresses->find()->select(['Id'])->where(['LOWER(Tag)' => strtolower($entity->Tag)])->first();
+ if ($addrTag) {
+ return $this->_jsonError('Oops! The tag is already taken. Please specify a different tag.', 400);
+ }
+
+ // check for existing verification
+ $exist = $this->TagAddressRequests->find()->select(['Id'])->where(['Address' => $base58address, 'IsVerified' => 0])->first();
+ if ($exist) {
+ return $this->_jsonError('Oops! There is a pending tag verification for this address.', 400);
+ }
+
+ // save the request
+ if (!$this->TagAddressRequests->save($entity)) {
+ return $this->_jsonError('Oops! The verification request could not be saved. If this problem persists, please send an email to hello@aureolin.co');
+ }
+
+ return $this->_jsonResponse(['success' => true, 'tag' => $entity->Tag]);
+ }
+
+ public function apiaddrutxo($base58address = null) {
+ $this->autoRender = false;
+ $this->loadModel('Addresses');
+
+ if (!isset($base58address)) {
+ return $this->_jsonError('Base58 address not specified.', 400);
+ }
+
+ $arr = explode(',', $base58address);
+ $addresses = $this->Addresses->find()->select(['Id'])->where(['Address IN' => $arr])->toArray();
+ if (count($addresses) == 0) {
+ return $this->_jsonError('No base58 address matching the specified parameter was found.', 404);
+ }
+
+ $addressIds = [];
+ $params = [];
+ foreach ($addresses as $address) {
+ $addressIds[] = $address->Id;
+ $params[] = '?';
+ }
+
+ // Get the unspent outputs for the address
+ $conn = ConnectionManager::get('default');
+ $stmt = $conn->execute(sprintf(
+ 'SELECT T.Hash AS TransactionHash, O.Vout, O.Value, O.Addresses, O.ScriptPubKeyAsm, O.ScriptPubKeyHex, O.Type, O.RequiredSignatures, B.Confirmations ' .
+ 'FROM Transactions T ' .
+ 'JOIN Outputs O ON O.TransactionId = T.Id ' .
+ 'JOIN Blocks B ON B.Hash = T.BlockHash ' .
+ 'WHERE O.Id IN (SELECT OutputId FROM OutputsAddresses WHERE AddressId IN (%s)) AND O.IsSpent <> 1 ORDER BY T.TransactionTime ASC', implode(',', $params)), $addressIds);
+ $outputs = $stmt->fetchAll(\PDO::FETCH_OBJ);
+
+ $utxo = [];
+ foreach ($outputs as $out) {
+ $utxo[] = [
+ 'transaction_hash' => $out->TransactionHash,
+ 'output_index' => $out->Vout,
+ 'value' => (int) bcmul($out->Value, 100000000),
+ 'addresses' => json_decode($out->Addresses),
+ 'script' => $out->ScriptPubKeyAsm,
+ 'script_hex' => $out->ScriptPubKeyHex,
+ 'script_type' => $out->Type,
+ 'required_signatures' => (int) $out->RequiredSignatures,
+ 'spent' => false,
+ 'confirmations' => (int) $out->Confirmations
+ ];
+ }
+
+ return $this->_jsonResponse(['success' => true, 'utxo' => $utxo]);
+ }
+
+ protected function _jsonResponse($object = [], $statusCode = null)
+ {
+ $this->response->statusCode($statusCode);
+ $this->response->type('json');
+ $this->response->body(json_encode($object));
+ }
+
+ protected function _jsonError($message, $statusCode = null) {
+ return $this->_jsonResponse(['error' => true, 'message' => $message], $statusCode);
+ }
+
+ private static function curl_json_post($url, $data, $headers = []) {
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+
+ $response = curl_exec($ch);
+ if ($response === false) {
+ $error = curl_error($ch);
+ $errno = curl_errno($ch);
+ curl_close($ch);
+
+ throw new \Exception(sprintf('The request failed: %s', $error), $errno);
+ } else {
+ curl_close($ch);
+ }
+
+ // Close any open file handle
+ return $response;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/src/Controller/PagesController.php b/src/Controller/PagesController.php
new file mode 100644
index 0000000..55792c8
--- /dev/null
+++ b/src/Controller/PagesController.php
@@ -0,0 +1,69 @@
+redirect('/');
+ }
+ if (in_array('..', $path, true) || in_array('.', $path, true)) {
+ throw new ForbiddenException();
+ }
+ $page = $subpage = null;
+
+ if (!empty($path[0])) {
+ $page = $path[0];
+ }
+ if (!empty($path[1])) {
+ $subpage = $path[1];
+ }
+ $this->set(compact('page', 'subpage'));
+
+ try {
+ $this->render(implode('/', $path));
+ } catch (MissingTemplateException $e) {
+ if (Configure::read('debug')) {
+ throw $e;
+ }
+ throw new NotFoundException();
+ }
+ }
+}
diff --git a/src/Model/Behavior/SimpleAuditBehavior.php b/src/Model/Behavior/SimpleAuditBehavior.php
new file mode 100644
index 0000000..9f8a5b9
--- /dev/null
+++ b/src/Model/Behavior/SimpleAuditBehavior.php
@@ -0,0 +1,67 @@
+ false,
+ 'implementedMethods' => ['audit' => 'audit'],
+ 'fieldMap' => [
+ 'CreatedOn' => 'Created',
+ 'ModifiedOn' => 'Modified',
+ 'CreatedBy' => 'CreatedBy',
+ 'ModifiedBy' => 'ModifiedBy'
+ ]
+ ];
+
+ public function audit(Entity $entity, $systemOperation = false) {
+ $time = $this->_currentUtcTime()->format('Y-m-d H:i:s');
+ $user = ($systemOperation) ? self::$DefaultUser : $this->_currentUser();
+
+ if (!$systemOperation
+ && $this->_config['abortOnUserInvalid']
+ && $user == self::$DefaultUser)
+ {
+ return false;
+ }
+
+ $fieldMap = $this->_config['fieldMap'];
+
+ if ($entity->isNew()) {
+ $entity->set($fieldMap['CreatedOn'], $time);
+ $entity->set($fieldMap['CreatedBy'], $user);
+ }
+
+ $entity->set($fieldMap['ModifiedOn'], $time);
+ $entity->set($fieldMap['ModifiedBy'], $user);
+ return true;
+ }
+
+ public function beforeSave(Event $event, Entity $entity) {
+ return $this->audit($entity);
+ }
+
+ private function _currentUtcTime() {
+ return new \DateTime('now', new \DateTimeZone('UTC'));
+ }
+
+ private function _currentUser() {
+ $request = Router::getRequest(true);
+ if ($request) {
+ $session = $request->session();
+ $fieldValue = $session->read(sprintf('Auth.User.' . $this->_userField));
+ return (intval($fieldValue) > 0) ? $fieldValue : self::$DefaultUser;
+ }
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/src/Model/Entity/Address.php b/src/Model/Entity/Address.php
new file mode 100644
index 0000000..d5b900a
--- /dev/null
+++ b/src/Model/Entity/Address.php
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/src/Model/Entity/Block.php b/src/Model/Entity/Block.php
new file mode 100644
index 0000000..7858813
--- /dev/null
+++ b/src/Model/Entity/Block.php
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/src/Model/Entity/Input.php b/src/Model/Entity/Input.php
new file mode 100644
index 0000000..c6f7d89
--- /dev/null
+++ b/src/Model/Entity/Input.php
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/src/Model/Entity/Output.php b/src/Model/Entity/Output.php
new file mode 100644
index 0000000..5ad3732
--- /dev/null
+++ b/src/Model/Entity/Output.php
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/src/Model/Entity/TagAddressRequest.php b/src/Model/Entity/TagAddressRequest.php
new file mode 100644
index 0000000..5f7977f
--- /dev/null
+++ b/src/Model/Entity/TagAddressRequest.php
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/src/Model/Entity/Transaction.php b/src/Model/Entity/Transaction.php
new file mode 100644
index 0000000..99f5aab
--- /dev/null
+++ b/src/Model/Entity/Transaction.php
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/src/Model/Table/AddressesTable.php b/src/Model/Table/AddressesTable.php
new file mode 100644
index 0000000..1876136
--- /dev/null
+++ b/src/Model/Table/AddressesTable.php
@@ -0,0 +1,18 @@
+primaryKey('Id');
+ $this->table('Addresses');
+
+ $this->addBehavior('SimpleAudit');
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/src/Model/Table/BlocksTable.php b/src/Model/Table/BlocksTable.php
new file mode 100644
index 0000000..0b2a8ed
--- /dev/null
+++ b/src/Model/Table/BlocksTable.php
@@ -0,0 +1,18 @@
+primaryKey('Id');
+ $this->table('Blocks');
+
+ $this->addBehavior('SimpleAudit');
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/src/Model/Table/InputsTable.php b/src/Model/Table/InputsTable.php
new file mode 100644
index 0000000..c1c06e7
--- /dev/null
+++ b/src/Model/Table/InputsTable.php
@@ -0,0 +1,30 @@
+primaryKey('Id');
+ $this->table('Inputs');
+
+ $this->addBehavior('SimpleAudit');
+
+ $this->addAssociations([
+ 'belongsToMany' => [
+ 'InputAddresses' => [
+ 'className' => 'App\Model\Table\AddressesTable',
+ 'joinTable' => 'InputsAddresses',
+ 'foreignKey' => 'InputId',
+ 'targetForeignKey' => 'AddressId',
+ 'propertyName' => 'InputAddresses'
+ ]
+ ]
+ ]);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/src/Model/Table/OutputsTable.php b/src/Model/Table/OutputsTable.php
new file mode 100644
index 0000000..1840aa7
--- /dev/null
+++ b/src/Model/Table/OutputsTable.php
@@ -0,0 +1,37 @@
+primaryKey('Id');
+ $this->table('Outputs');
+
+ $this->addBehavior('SimpleAudit');
+
+ $this->addAssociations([
+ 'belongsTo' => [
+ 'SpendInput' => [
+ 'className' => 'App\Model\Table\InputsTable',
+ 'foreignKey' => 'SpentByInputId',
+ 'propertyName' => 'SpendInput'
+ ]
+ ],
+ 'belongsToMany' => [
+ 'OutputAddresses' => [
+ 'className' => 'App\Model\Table\AddressesTable',
+ 'joinTable' => 'OutputsAddresses',
+ 'foreignKey' => 'OutputId',
+ 'targetForeignKey' => 'AddressId',
+ 'propertyName' => 'OutputAddresses'
+ ]
+ ]
+ ]);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/src/Model/Table/TagAddressRequestsTable.php b/src/Model/Table/TagAddressRequestsTable.php
new file mode 100644
index 0000000..f8dc755
--- /dev/null
+++ b/src/Model/Table/TagAddressRequestsTable.php
@@ -0,0 +1,18 @@
+primaryKey('Id');
+ $this->table('TagAddressRequests');
+
+ $this->addBehavior('SimpleAudit');
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/src/Model/Table/TransactionsTable.php b/src/Model/Table/TransactionsTable.php
new file mode 100644
index 0000000..613512d
--- /dev/null
+++ b/src/Model/Table/TransactionsTable.php
@@ -0,0 +1,18 @@
+primaryKey('Id');
+ $this->table('Transactions');
+
+ $this->addBehavior('SimpleAudit');
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/src/Shell/AuxShell.php b/src/Shell/AuxShell.php
new file mode 100644
index 0000000..98d003b
--- /dev/null
+++ b/src/Shell/AuxShell.php
@@ -0,0 +1,140 @@
+loadModel('Addresses');
+ $this->loadModel('Inputs');
+ $this->loadModel('Outputs');
+ $this->loadModel('Transactions');
+ $this->loadModel('TagAddressRequests');
+ }
+
+ public function main() {
+ echo "No arguments specified.\n";
+ }
+
+ public function verifytags() {
+ self::lock('auxverify');
+
+ $conn = ConnectionManager::get('default');
+ $requests = $this->TagAddressRequests->find()->where(['IsVerified <>' => 1])->toArray();
+ foreach ($requests as $req) {
+ echo "Verifying tag for $req->Address, amount: $req->VerificationAmount... ";
+
+ $req_date = $req->Created;
+ $src_addr = $req->Address;
+ $dst_addr = self::tagrcptaddress;
+
+ // find a transaction with the corresponding inputs created on or after the date
+ // look for the address ids
+ $address = $this->Addresses->find()->select(['Id'])->where(['Address' => $src_addr])->first();
+ $veri_address = $this->Addresses->find()->select(['Id'])->where(['Address' => $dst_addr])->first(); // TODO: Redis cache?
+ if (!$address || !$veri_address) {
+ echo "could not find source nor verification addresses. Skipping.\n";
+ continue;
+ }
+
+ $src_addr_id = $address->Id;
+ $dst_addr_id = $veri_address->Id;
+
+ // find the inputs for the source address that were created after $req->Created - 1 hour
+ $req_date->sub(new \DateInterval('PT1H'));
+ $stmt = $conn->execute('SELECT DISTINCT I.TransactionId FROM Inputs I ' .
+ 'RIGHT JOIN (SELECT IIA.InputId FROM InputsAddresses IIA WHERE IIA.AddressId = ?) IA ON IA.InputId = I.Id ' .
+ 'JOIN Transactions T ON T.Id = I.TransactionId ' .
+ 'LEFT JOIN Blocks B ON B.Hash = T.BlockHash ' .
+ 'WHERE B.Confirmations > 0 AND DATE(I.Created) >= ?', [$src_addr_id, $req_date->format('Y-m-d')]);
+ $tx_inputs = $stmt->fetchAll(\PDO::FETCH_OBJ);
+
+
+ $param_values = [$dst_addr_id];
+ $params = [];
+ foreach ($tx_inputs as $in) {
+ $params[] = '?';
+ $param_values[] = $in->TransactionId;
+ }
+
+ $num_inputs = count($tx_inputs);
+ echo "***found $num_inputs inputs from address $src_addr.\n";
+
+ if ($num_inputs == 0) {
+ continue;
+ }
+
+ try {
+ // check the outputs with the dst address
+ $total_amount = 0;
+ $stmt = $conn->execute(sprintf('SELECT O.Value FROM Outputs O ' .
+ 'RIGHT JOIN (SELECT IOA.OutputId, IOA.AddressId FROM OutputsAddresses IOA WHERE IOA.AddressId = ?) OA ON OA.OutputId = O.Id ' .
+ 'WHERE O.TransactionId IN (%s)', implode(', ', $params)), $param_values);
+ $tx_outputs = $stmt->fetchAll(\PDO::FETCH_OBJ);
+ foreach ($tx_outputs as $out) {
+ echo "***found output to verification address with value " . $out->Value . "\n";
+ $total_amount = bcadd($total_amount, $out->Value, 8);
+ }
+
+ if ($total_amount >= $req->VerificationAmount) {
+ $conn->begin();
+ echo "***$total_amount is gte verification amount: $req->VerificationAmount.\n";
+
+ // Update the tag in the DB
+ $conn->execute('UPDATE Addresses SET Tag = ?, TagUrl = ? WHERE Address = ?', [$req->Tag, $req->TagUrl, $src_addr]);
+
+ // Set the request as verified
+ $conn->execute('UPDATE TagAddressRequests SET IsVerified = 1 WHERE Id = ?', [$req->Id]);
+
+ $conn->commit();
+ echo "Data committed.\n";
+ } else {
+ echo "***$total_amount is NOT up to verification amount: $req->VerificationAmount.\n";
+ }
+ } catch (\Exception $e) {
+ print_r($e);
+ echo "Rolling back.\n";
+ $conn->rollback();
+ }
+ }
+
+ self::unlock('auxverify');
+ }
+
+ public static function lock($process_name) {
+ if (!is_dir(TMP . 'lock')) {
+ mkdir(TMP . 'lock');
+ }
+ $lock_file = TMP . 'lock' . DS . $process_name;
+ if (file_exists($lock_file)) {
+ echo "$process_name is already running.\n";
+ exit(0);
+ }
+ file_put_contents($lock_file, '1');
+ }
+
+ public static function unlock($process_name) {
+ $lock_file = TMP . 'lock' . DS . $process_name;
+ if (file_exists($lock_file)) {
+ unlink($lock_file);
+ }
+ return true;
+ }
+}
+
+?>
diff --git a/src/Shell/BlockShell.php b/src/Shell/BlockShell.php
new file mode 100644
index 0000000..c5102e7
--- /dev/null
+++ b/src/Shell/BlockShell.php
@@ -0,0 +1,1707 @@
+loadModel('Blocks');
+ $this->loadModel('Addresses');
+ $this->loadModel('Inputs');
+ $this->loadModel('Outputs');
+ $this->loadModel('Transactions');
+ }
+
+ public function main() {
+ //$this->parsenewblocks();
+ $this->out('No arguments specified');
+ }
+
+ public function testtx() {
+ $raw = '0100000001e4801fa8c9621753410e6c576c73168cc551624e08e4cc56e9a9189e8ebabcc3010000006b483045022100dcc5ae5564353e05cd8415936cdb534a26049a80a9866dbcab5dbb6ab16abbc702203af681fb5fbba5c78af887778aa32f27bc653312322b44c2a78e883e322921790121031c38def6b103b58481818d7d8aba39f9f51798f686ec3332bac23726a23366adffffffff0200e1f50500000000fd3a01b512626c6f636b6578706c6f7265722d686f6d654d0801080110011aa3010801125c080410011a1c4c42525920426c6f636b204578706c6f72657220486f6d6570616765222c574950204c42525920426c6f636b204578706c6f72657220686f6d6520706167652073637265656e73686f742a00320038004a0052005a001a41080110011a3038c6bf4a310d5b172aaae03cffb93e5a27c47f3946f58af0a4b9f99d7be42998f7dcec9b0fb0c124a6477bdb4c5311db2209696d6167652f706e672a5c080110031a401eb69f65768ba2cc347067dfb1317131137f7d685a5e804a4aeedd2385332c70f3ec7632fcebfbc073e37a58dbfbf4e6641853bbfc188b0d8bf27435b540801d22146b2a1d378efcdb7db7a03baa7dfedb86b976bc4a6d7576a914b7182b0f7c896ab240d233f81cb891e3f25e739988ac8085e811000000001976a9146fde27b4dac0b79a9ea06756562a52b019da7a8e88ac00000000';
+ $decoded = self::decode_tx($raw);
+ }
+
+ public function fixzerooutputs() {
+ self::lock('zerooutputs');
+
+ $redis = new \Predis\Client('tcp://127.0.0.1:6379');
+ $conn = ConnectionManager::get('default');
+
+ /** 2017-06-12 21:38:07 **/
+ $last_fixed_txid = $redis->exists('fix.txid') ? $redis->get('fix.txid') : 0;
+ try {
+ $stmt = $conn->execute('SELECT Id FROM Transactions WHERE Id > ? AND Created <= ? LIMIT 1000000', [$last_fixed_txid, '2017-06-12 21:38:07']);
+ $txids = $stmt->fetchAll(\PDO::FETCH_OBJ);
+
+ $count = count($txids);
+ $idx = 0;
+ foreach ($txids as $distincttx) {
+ $idx++;
+ $idx_str = str_pad($idx, strlen($count), '0', STR_PAD_LEFT);
+ $txid = $distincttx->Id;
+ echo "[$idx_str/$count] Processing txid: $txid... ";
+ $total_diff = 0;
+
+ // findtx
+ $start_ms = round(microtime(true) * 1000);
+ $tx = $this->Transactions->find()->select(['Hash'])->where(['Id' => $txid])->first();
+ $diff_ms = (round(microtime(true) * 1000)) - $start_ms;
+ $total_diff += $diff_ms;
+ echo "findtx took {$diff_ms}ms. ";
+
+ // Get the inputs and outputs
+ // Get the raw transaction (Use getrawtx daemon instead (for speed!!!)
+
+ // getraw
+ $req = ['method' => 'getrawtransaction', 'params' => [$tx->Hash]];
+ $start_ms = round(microtime(true) * 1000);
+ $response = self::curl_json_post(self::rpcurl, json_encode($req));
+ $diff_ms = (round(microtime(true) * 1000)) - $start_ms;
+ $total_diff += $diff_ms;
+ echo "getrawtx took {$diff_ms}ms. ";
+
+ $start_ms = round(microtime(true) * 1000);
+ $json = json_decode($response);
+ $tx_result = $json->result;
+ $raw_tx = $tx_result;
+ $tx_data = self::decode_tx($raw_tx);
+
+ $all_tx_data = $this->txdb_data_from_decoded($tx_data);
+
+ $inputs = $all_tx_data['inputs'];
+ $outputs = $all_tx_data['outputs'];
+
+ $addr_id_map = [];
+ $addr_id_drcr = []; // debits and credits grouped by address
+ $total_tx_value = 0;
+
+ $diff_ms = (round(microtime(true) * 1000)) - $start_ms;
+ $total_diff += $diff_ms;
+ echo "decodetx took {$diff_ms}ms. ";
+
+ $start_ms = round(microtime(true) * 1000);
+ $num_outputs = count($outputs);
+ foreach ($outputs as $out) {
+ $vout = $out['Vout'];
+ $total_tx_value = bcadd($total_tx_value, $out['Value'], 8);
+
+ // Update the output
+ //$conn->execute('UPDATE Outputs SET Value = ? WHERE TransactionId = ? AND Vout = ?', [$out['Value'], $txid, $vout]);
+
+ $json_addr = json_decode($out['Addresses']);
+ $address = $json_addr[0];
+
+ // Get the address ID
+ $addr_id = -1;
+ if (isset($addr_id_map[$address])) {
+ $addr_id = $addr_id_map[$address];
+ } else {
+ $src_addr = $this->Addresses->find()->select(['Id'])->where(['Address' => $address])->first();
+ if ($src_addr) {
+ $addr_id = $src_addr->Id;
+ $addr_id_map[$address] = $addr_id;
+ }
+ }
+
+ if ($addr_id > -1) {
+ if (!isset($addr_id_drcr[$addr_id])) {
+ $addr_id_drcr[$addr_id] = ['debit' => 0, 'credit' => 0];
+ }
+ $addr_id_drcr[$addr_id]['credit'] = bcadd($addr_id_drcr[$addr_id]['credit'], $out['Value'], 8);
+
+ // Update the Received amount for the address based on the output
+ $conn->execute('UPDATE Addresses SET TotalReceived = TotalReceived + ? WHERE Id = ?', [$out['Value'], $addr_id]);
+ }
+ }
+ $diff_ms = (round(microtime(true) * 1000)) - $start_ms;
+ $total_diff += $diff_ms;
+ echo "$num_outputs output(s) took {$diff_ms}ms. ";
+
+ // Fix the input values
+ $start_ms = round(microtime(true) * 1000);
+ $num_inputs = count($inputs);
+ foreach ($inputs as $in) {
+ if (isset($in['PrevoutHash'])) {
+ $prevout_hash = $in['PrevoutHash'];
+ $in_prevout = $in['PrevoutN'];
+ $prevout_tx_id = -1;
+ $prevout_tx = $this->Transactions->find()->select(['Id'])->where(['Hash' => $prevout_hash])->first();
+ if (!$prevout_tx) {
+ continue;
+ }
+
+ $prevout_tx_id = $prevout_tx->Id;
+ $stmt = $conn->execute('SELECT Value, Addresses FROM Outputs WHERE TransactionId = ? AND Vout = ?', [$prevout_tx_id, $in_prevout]);
+ $src_output = $stmt->fetch(\PDO::FETCH_OBJ);
+ if ($src_output) {
+ $in['Value'] = $src_output->Value;
+ //$conn->execute('UPDATE Inputs SET Value = ? WHERE TransactionId = ? AND PrevoutHash = ? AND PrevoutN = ?', [$in['Value'], $txid, $prevout_hash, $in_prevout]);
+ $json_addr = json_decode($src_output->Addresses);
+ $address = $json_addr[0];
+
+ // Get the address ID
+ $addr_id = -1;
+ if (isset($addr_id_map[$address])) {
+ $addr_id = $addr_id_map[$address];
+ } else {
+ $src_addr = $this->Addresses->find()->select(['Id'])->where(['Address' => $address])->first();
+ if ($src_addr) {
+ $addr_id = $src_addr->Id;
+ $addr_id_map[$address] = $addr_id;
+ }
+ }
+
+ if ($addr_id > -1) {
+ if (!isset($addr_id_drcr[$addr_id])) {
+ $addr_id_drcr[$addr_id] = ['debit' => 0, 'credit' => 0];
+ }
+ $addr_id_drcr[$addr_id]['debit'] = bcadd($addr_id_drcr[$addr_id]['debit'], $in['Value'], 8);
+
+ // Update total sent
+ $conn->execute('UPDATE Addresses SET TotalSent = TotalSent + ? WHERE Id = ?', [$in['Value'], $addr_id]);
+ }
+ }
+ }
+ }
+ $diff_ms = (round(microtime(true) * 1000)) - $start_ms;
+ $total_diff += $diff_ms;
+ echo "$num_inputs input(s) took {$diff_ms}ms. ";
+
+ // update tx time
+ /*$start_ms = round(microtime(true) * 1000);
+ $upd_addr_ids = [];
+ $conn->execute('UPDATE Transactions SET Value = ? WHERE Id = ?', [$total_tx_value, $txid]);
+ foreach ($addr_id_drcr as $addr_id => $drcr) {
+ try {
+ if (!isset($upd_addr_ids[$addr_id])) {
+ $upd_addr_ids[$addr_id] = 1;
+ $conn->execute('UPDATE TransactionsAddresses SET TransactionTime = (SELECT FROM_UNIXTIME(TransactionTime) FROM Transactions WHERE Id = ? AND TransactionTime IS NOT NULL)', [$txid]);
+ }
+
+ } catch (Exception $e) {
+ print_r($e);
+ $data_error = true;
+ break;
+ }
+ }*/
+
+ $redis->set('fix.txid', $txid);
+ $diff_ms = (round(microtime(true) * 1000)) - $start_ms;
+ $total_diff += $diff_ms;
+ echo "update took {$diff_ms}ms. Total {$total_diff} ms.\n";
+ }
+ } catch (\Exception $e) {
+ print_r($e);
+ }
+
+ self::unlock('zerooutputs');
+ }
+
+ public function addrtxamounts() {
+ set_time_limit(0);
+
+ self::lock('addrtxamounts');
+
+ try {
+ $conn = ConnectionManager::get('default');
+ $stmt = $conn->execute('SELECT TransactionId, AddressId FROM TransactionsAddresses WHERE DebitAmount = 0 AND CreditAmount = 0 LIMIT 1000000');
+ $txaddresses = $stmt->fetchAll(\PDO::FETCH_OBJ);
+
+ $count = count($txaddresses);
+ $idx = 0;
+
+ echo "Processing $count tx address combos...\n";
+ foreach ($txaddresses as $txaddr) {
+ $idx++;
+ $idx_str = str_pad($idx, strlen($count), '0', STR_PAD_LEFT);
+
+ // Check the inputs
+ $stmt = $conn->execute('SELECT SUM(I.Value) AS DebitAmount FROM Inputs I JOIN InputsAddresses IA ON IA.InputId = I.Id WHERE I.TransactionId = ? AND IA.AddressId = ?',
+ [$txaddr->TransactionId, $txaddr->AddressId]);
+ $res = $stmt->fetch(\PDO::FETCH_OBJ);
+ $debitamount = $res->DebitAmount ? $res->DebitAmount : 0;
+
+ $stmt = $conn->execute('SELECT SUM(O.Value) AS CreditAmount FROM Outputs O JOIN OutputsAddresses OA ON OA.OutputId = O.Id WHERE O.TransactionId = ? AND OA.AddressId = ?',
+ [$txaddr->TransactionId, $txaddr->AddressId]);
+ $res = $stmt->fetch(\PDO::FETCH_OBJ);
+ $creditamount = $res->CreditAmount ? $res->CreditAmount : 0;
+
+ echo "[$idx_str/$count] Updating tx $txaddr->TransactionId, address id $txaddr->AddressId with debit amount: $debitamount, credit amount: $creditamount... ";
+ $conn->execute('UPDATE TransactionsAddresses SET DebitAmount = ?, CreditAmount = ? WHERE TransactionId = ? AND AddressId = ?',
+ [$debitamount, $creditamount, $txaddr->TransactionId, $txaddr->AddressId]);
+ echo "Done.\n";
+ }
+ } catch (\Exception $e) {
+ // failed
+ print_r($e);
+ }
+
+ self::unlock('addrtxamounts');
+ }
+
+ private function processtx($tx_hash, $block_ts, $block_data, &$data_error) {
+ // Get the raw transaction (Use getrawtx daemon instead (for speed!!!)
+ $req = ['method' => 'getrawtransaction', 'params' => [$tx_hash]];
+ $response = self::curl_json_post(self::rpcurl, json_encode($req));
+ $json = json_decode($response);
+ $tx_result = $json->result;
+ $raw_tx = $tx_result;
+ $tx_data = self::decode_tx($raw_tx);
+
+ $all_tx_data = $this->txdb_data_from_decoded($tx_data);
+ $conn = ConnectionManager::get('default');
+
+ // Create / update addresses
+ $addr_id_map = [];
+ foreach($all_tx_data['addresses'] as $address => $address) {
+ $prev_addr = $this->Addresses->find()->select(['Id'])->where(['Address' => $address])->first();
+ if (!$prev_addr) {
+ $new_addr = [
+ 'Address' => $address,
+ 'FirstSeen' => $block_ts->format('Y-m-d H:i:s')
+ ];
+ $entity = $this->Addresses->newEntity($new_addr);
+ $res = $this->Addresses->save($entity);
+ if (!$res) {
+ $data_error = true;
+ } else {
+ $addr_id_map[$address] = $entity->Id;
+ }
+ } else {
+ $addr_id_map[$address] = $prev_addr->Id;
+ }
+ }
+
+ $addr_id_drcr = []; // debits and credits grouped by address
+ $numeric_tx_id = -1;
+ if (!$data_error) {
+ // Create transaction
+ $new_tx = $all_tx_data['tx'];
+
+ $total_tx_value = 0;
+ foreach ($all_tx_data['outputs'] as $out) {
+ $total_tx_value = bcadd($total_tx_value, $out['Value'], 8);
+ }
+
+ if ($block_data) {
+ $new_tx['BlockHash'] = $block_data['hash'];
+ $new_tx['TransactionTime'] = $block_data['time'];
+ }
+ $new_tx['TransactionSize'] = ((strlen($raw_tx)) / 2);
+ $new_tx['InputCount'] = count($all_tx_data['inputs']);
+ $new_tx['OutputCount'] = count($all_tx_data['outputs']);
+ $new_tx['Hash'] = $tx_hash;
+ $new_tx['Value'] = $total_tx_value;
+ $new_tx['Raw'] = $raw_tx;
+
+
+ $tx_entity = $this->Transactions->newEntity($new_tx);
+ $conn->execute('INSERT INTO Transactions (Version, LockTime, BlockHash, TransactionTime, InputCount, OutputCount, TransactionSize, Hash, Value, Raw, Created, Modified) VALUES ' .
+ '(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP(), UTC_TIMESTAMP())',
+ [
+ $new_tx['Version'],
+ $new_tx['LockTime'],
+ isset($new_tx['BlockHash']) ? $new_tx['BlockHash'] : null,
+ isset($new_tx['TransactionTime']) ? $new_tx['TransactionTime'] : null,
+ $new_tx['InputCount'],
+ $new_tx['OutputCount'],
+ $new_tx['TransactionSize'],
+ $new_tx['Hash'],
+ $new_tx['Value'],
+ $new_tx['Raw']
+ ]);
+ $stmt = $conn->execute('SELECT LAST_INSERT_ID() AS txnId');
+ $linsert = $stmt->fetch(\PDO::FETCH_OBJ);
+ $tx_entity->Id = $linsert->txnId;
+ if ($tx_entity->Id === 0) {
+ 3;
+ } else {
+ $numeric_tx_id = $tx_entity->Id;
+ }
+ }
+
+ if (!$data_error && $numeric_tx_id > 0) {
+ // Create the inputs
+ $inputs = $all_tx_data['inputs'];
+ $outputs = $all_tx_data['outputs'];
+
+ foreach ($inputs as $in) {
+ $in['TransactionId'] = $numeric_tx_id;
+ $in['TransactionHash'] = $tx_hash;
+
+ if (isset($in['IsCoinbase']) && $in['IsCoinbase'] === 1) {
+ $in_entity = $this->Inputs->newEntity($in);
+ $res = $this->Inputs->save($in_entity);
+ if (!$res) {
+ $data_error = true;
+ break;
+ }
+ } else {
+ $in_tx_hash = $in['PrevoutHash'];
+ $in_prevout = $in['PrevoutN'];
+
+ // Find the corresponding previous output
+ $in_tx = $this->Transactions->find()->select(['Id'])->where(['Hash' => $in_tx_hash])->first();
+ $src_output = null;
+ if ($in_tx) {
+ $stmt = $conn->execute('SELECT Id, Value, Addresses FROM Outputs WHERE TransactionId = ? AND Vout = ?', [$in_tx->Id, $in_prevout]);
+ $src_output = $stmt->fetch(\PDO::FETCH_OBJ);
+ if ($src_output) {
+ $in['Value'] = $src_output->Value;
+ $json_addr = json_decode($src_output->Addresses);
+ $in_addr_id = 0;
+ if (isset($addr_id_map[$json_addr[0]])) {
+ $in['AddressId'] = $addr_id_map[$json_addr[0]];
+ } else {
+ $in_addr = $this->Addresses->find()->select(['Id'])->where(['Address' => $json_addr[0]])->first();
+ if ($in_addr) {
+ $addr_id_map[$json_addr[0]] = $in_addr->Id;
+ $in['AddressId'] = $in_addr->Id;
+ }
+ }
+ }
+ }
+
+ $in_entity = $this->Inputs->newEntity($in);
+ $conn->execute('INSERT INTO Inputs (TransactionId, TransactionHash, AddressId, PrevoutHash, PrevoutN, Sequence, Value, ScriptSigAsm, ScriptSigHex, Created, Modified) ' .
+ 'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP(), UTC_TIMESTAMP())',
+ [$in['TransactionId'],
+ $in['TransactionHash'],
+ isset($in['AddressId']) ? $in['AddressId'] : null,
+ $in['PrevoutHash'],
+ $in['PrevoutN'],
+ $in['Sequence'],
+ isset($in['Value']) ? $in['Value'] : 0,
+ $in['ScriptSigAsm'],
+ $in['ScriptSigHex']
+ ]);
+
+ // get last insert id
+ $stmt = $conn->execute('SELECT LAST_INSERT_ID() AS inputId');
+ $linsert = $stmt->fetch(\PDO::FETCH_OBJ);
+ $in_entity->Id = $linsert->inputId;
+
+ if ($in_entity->Id === 0) {
+ $data_error = true;
+ break;
+ }
+
+ // Update the src_output spent if successful
+ if ($src_output) {
+ try {
+ $conn->execute('UPDATE Outputs SET IsSpent = 1, SpentByInputId = ? WHERE Id = ?', [$in_entity->Id, $src_output->Id]);
+ $conn->execute('UPDATE Inputs SET PrevoutSpendUpdated = 1 WHERE Id = ?', [$in_entity->Id]);
+ } catch (\Exception $e) {
+ $data_error = true;
+ break;
+ }
+ }
+
+ if (isset($in['AddressId']) && $in['AddressId'] > 0) {
+ $addr_id = $in['AddressId'];
+ if (!isset($addr_id_drcr[$addr_id])) {
+ $addr_id_drcr[$addr_id] = ['debit' => 0, 'credit' => 0];
+ }
+ $addr_id_drcr[$addr_id]['debit'] = bcadd($addr_id_drcr[$addr_id]['debit'], $in['Value'], 8);
+
+ try {
+ $conn->execute('REPLACE INTO InputsAddresses (InputId, AddressId) VALUES (?, ?)', [$in_entity->Id, $in['AddressId']]);
+ $conn->execute('UPDATE Addresses SET TotalSent = TotalSent + ? WHERE Id = ?', [$in['Value'], $in['AddressId']]);
+ $conn->execute('INSERT INTO TransactionsAddresses (TransactionId, AddressId) VALUES (?, ?) ON DUPLICATE KEY UPDATE TransactionId = TransactionId', [$numeric_tx_id, $in['AddressId']]);
+ } catch (\Exception $e) {
+ $data_error = true;
+ break;
+ }
+ }
+ }
+ }
+
+ foreach ($outputs as $out) {
+ $out['TransactionId'] = $numeric_tx_id;
+ $out_entity = $this->Outputs->newEntity($out);
+
+ //$stmt->execute('INSERT INTO Outputs')
+ $conn->execute('INSERT INTO Outputs (TransactionId, Vout, Value, Type, ScriptPubKeyAsm, ScriptPubKeyHex, RequiredSignatures, Hash160, Addresses, Created, Modified) '.
+ 'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP(), UTC_TIMESTAMP())',
+ [$out['TransactionId'],
+ $out['Vout'],
+ $out['Value'],
+ $out['Type'],
+ $out['ScriptPubKeyAsm'],
+ $out['ScriptPubKeyHex'],
+ $out['RequiredSignatures'],
+ $out['Hash160'],
+ $out['Addresses']
+ ]);
+
+ // get the last insert id
+ $stmt = $conn->execute('SELECT LAST_INSERT_ID() AS outputId');
+ $linsert = $stmt->fetch(\PDO::FETCH_OBJ);
+ $out_entity->Id = $linsert->outputId;
+
+ if ($out_entity->Id === 0) {
+ $data_error = true;
+ break;
+ }
+
+ $json_addr = json_decode($out['Addresses']);
+ $out_addr_id = 0;
+ if (isset($addr_id_map[$json_addr[0]])) {
+ $out_addr_id = $addr_id_map[$json_addr[0]];
+ } else {
+ $out_addr = $this->Addresses->find()->select(['Id'])->where(['Address' => $json_addr[0]])->first();
+ if ($out_addr) {
+ $addr_id_map[$json_addr[0]] = $out_addr->Id;
+ $out_addr_id = $out_addr->Id;
+ }
+ }
+
+ if ($out_addr_id > 0) {
+ $addr_id = $out_addr_id;
+ if (!isset($addr_id_drcr[$addr_id])) {
+ $addr_id_drcr[$addr_id] = ['debit' => 0, 'credit' => 0];
+ }
+ $addr_id_drcr[$addr_id]['credit'] = bcadd($addr_id_drcr[$addr_id]['credit'], $out['Value'], 8);
+
+ try {
+ $conn->execute('REPLACE INTO OutputsAddresses (OutputId, AddressId) VALUES (?, ?)', [$out_entity->Id, $out_addr_id]);
+ $conn->execute('UPDATE Addresses SET TotalReceived = TotalReceived + ? WHERE Id = ?', [$out['Value'], $out_addr_id]);
+ $conn->execute('INSERT INTO TransactionsAddresses (TransactionId, AddressId) VALUES (?, ?) ON DUPLICATE KEY UPDATE TransactionId = TransactionId', [$numeric_tx_id, $out_addr_id]);
+ } catch (\Exception $e) {
+ print_r($e);
+ $data_error = true;
+ break;
+ }
+ }
+ }
+ }
+
+ // update tx amounts
+ if (!$data_error) {
+ foreach ($addr_id_drcr as $addr_id => $drcr) {
+ try {
+ $conn->execute('UPDATE TransactionsAddresses SET DebitAmount = ?, CreditAmount = ?, TransactionTime = UTC_TIMESTAMP() WHERE TransactionId = ? AND AddressId = ?',
+ [$drcr['debit'], $drcr['credit'], $numeric_tx_id, $addr_id]);
+ } catch (Exception $e) {
+ print_r($e);
+ $data_error = true;
+ break;
+ }
+ }
+ }
+ }
+
+ public function parsetxs() {
+ set_time_limit(0);
+
+ self::lock('parsetxs');
+
+ // Get the minimum block with no processed transactions
+ echo "Parsing transactions...\n";
+
+ $conn = ConnectionManager::get('default');
+ //$conn->execute('SET foreign_key_checks = 0');
+ //$conn->execute('SET unique_checks = 0');
+
+ try {
+ $unproc_blocks = $this->Blocks->find()->select(['Id', 'Height', 'Hash', 'TransactionHashes', 'BlockTime'])->where(['TransactionsProcessed' => 0])->order(['Height' => 'asc'])->toArray();
+ foreach ($unproc_blocks as $min_block) {
+ $tx_hashes = json_decode($min_block->TransactionHashes);
+ if ($tx_hashes && is_array($tx_hashes)) {
+ $block_time = $min_block->BlockTime;
+ $block_ts = \DateTime::createFromFormat('U', $block_time);
+
+ $count = count($tx_hashes);
+ echo "Processing " . $count . " transaction(s) for block $min_block->Height ($min_block->Hash)...\n";
+
+ $data_error = false;
+ $conn->begin();
+
+ $idx = 0;
+ foreach ($tx_hashes as $tx_hash) {
+ $idx++;
+ $idx_str = str_pad($idx, strlen($count), '0', STR_PAD_LEFT);
+ echo "[$idx_str/$count] Processing tx hash: $tx_hash... ";
+
+ $total_diff = 0;
+ $start_ms = round(microtime(true) * 1000);
+ $exist_tx = $this->Transactions->find()->select(['Id'])->where(['Hash' => $tx_hash])->first();
+ $end_ms = round(microtime(true) * 1000);
+ $diff_ms = $end_ms - $start_ms;
+ $total_diff += $diff_ms;
+ echo "findtx took {$diff_ms}ms. ";
+
+ if ($exist_tx) {
+ echo "Exists. Skipping.\n";
+ continue;
+ }
+
+ $start_ms = round(microtime(true) * 1000);
+ $this->processtx($tx_hash, $block_ts, ['hash' => $min_block->Hash, 'time' => $min_block->BlockTime], $data_error);
+ $diff_ms = round(microtime(true) * 1000) - $start_ms;
+ $total_diff += $diff_ms;
+ echo "tx took {$diff_ms}ms. Total {$total_diff}ms. ";
+
+ echo "Done.\n";
+ }
+
+ if (!$data_error) {
+ $conn->execute('UPDATE Blocks SET TransactionsProcessed = 1 WHERE Id = ?', [$min_block->Id]);
+ }
+
+ if ($data_error) {
+ echo "Rolling back!\n";
+ $conn->rollback();
+ throw new \Exception('Data save failed!');
+ } else {
+ echo "Data committed.\n";
+ $conn->commit();
+ }
+ }
+ }
+
+ // Try to update txs with null BlockHash
+ $mempooltx = $this->Transactions->find()->select(['Id', 'Hash'])->where(['BlockHash IS' => null])->order(['Created' => 'asc'])->toArray();
+ $idx = 0;
+ $count = count($mempooltx);
+ foreach ($mempooltx as $tx) {
+ $idx++;
+ $tx_hash = $tx->Hash;
+ $idx_str = ($count > 10 && $idx < 10) ? '0' . $idx : $idx;
+ echo "[$idx_str/$count] Processing tx hash: $tx_hash... ";
+
+ $stmt = $conn->execute("SELECT Hash, BlockTime FROM Blocks WHERE TransactionHashes LIKE CONCAT('%', ?, '%') AND Height > ((SELECT MAX(Height) FROM Blocks) - 2000) ORDER BY Height ASC LIMIT 1", [$tx_hash]);
+ $block = $stmt->fetch(\PDO::FETCH_OBJ);
+ if ($block) {
+ $upd_tx = ['Id' => $tx->Id, 'BlockHash' => $block->Hash, 'TransactionTime' => $block->BlockTime];
+ $upd_entity = $this->Transactions->newEntity($upd_tx);
+ $this->Transactions->save($upd_entity);
+ echo "Done.\n";
+ } else {
+ echo "Block not found.\n";
+ }
+ }
+ } catch (\Exception $e) {
+ print_r($e);
+ }
+
+ //$conn->execute('SET foreign_key_checks = 1');
+ //$conn->execute('SET unique_checks = 1');
+
+ self::unlock('parsetxs');
+ }
+
+ public function parsenewblocks() {
+ set_time_limit(0);
+ self::lock('parsenewblocks');
+
+ echo "Parsing new blocks...\n";
+ try {
+ // Get the best block hash
+ $req = ['method' => 'getbestblockhash', 'params' => []];
+ $response = self::curl_json_post(self::rpcurl, json_encode($req));
+ $json = json_decode($response);
+ $best_hash = $json->result;
+
+ $req = ['method' => 'getblock', 'params' => [$best_hash]];
+ $response = self::curl_json_post(self::rpcurl, json_encode($req));
+ $json = json_decode($response);
+ $best_block = $json->result;
+
+ $max_block = $this->Blocks->find()->select(['Hash', 'Height'])->order(['Height' => 'desc'])->first();
+ if (!$max_block) {
+ self::unlock('parsenewblocks');
+ return;
+ }
+
+ $min_height = min($max_block->Height, $best_block->height);
+ $max_height = max($max_block->Height, $best_block->height);
+ $height_diff = $best_block->height - $max_block->Height;
+
+ if ($height_diff <= 0) {
+ self::unlock('parsenewblocks');
+ return;
+ }
+
+ $conn = ConnectionManager::get('default');
+ for ($curr_height = $min_height; $curr_height <= $max_height; $curr_height++) {
+ // get the block hash
+ $req = ['method' => 'getblockhash', 'params' => [$curr_height]];
+ $response = self::curl_json_post(self::rpcurl, json_encode($req));
+ $json = json_decode($response);
+ $curr_block_hash = $json->result;
+
+ $next_block_hash = null;
+ if ($curr_height < $max_height) {
+ $req = ['method' => 'getblockhash', 'params' => [$curr_height + 1]];
+ $response = self::curl_json_post(self::rpcurl, json_encode($req));
+ $json = json_decode($response);
+ $next_block_hash = $json->result;
+ }
+
+ $req = ['method' => 'getblock', 'params' => [$curr_block_hash]];
+ $response = self::curl_json_post(self::rpcurl, json_encode($req));
+ $json = json_decode($response);
+ $curr_block = $json->result;
+
+ if ($curr_block->confirmations < 0) {
+ continue;
+ }
+
+ $next_block = null;
+ if ($next_block_hash != null) {
+ $req = ['method' => 'getblock', 'params' => [$next_block_hash]];
+ $response = self::curl_json_post(self::rpcurl, json_encode($req));
+ $json = json_decode($response);
+ $next_block = $json->result;
+ }
+
+ if ($curr_block != null) {
+ $curr_block_ins = $this->blockdb_data_from_json($curr_block);
+ if ($next_block != null && $curr_block_ins['NextBlockHash'] == null) {
+ $curr_block_ins['NextBlockHash'] = $next_block->hash;
+ }
+
+ $block_data = $curr_block;
+ $block_id = -1;
+ // Make sure the block does not exist before inserting
+ $old_block = $this->Blocks->find()->select(['Id'])->where(['Hash' => $block_data->hash])->first();
+ if (!$old_block) {
+ echo "Inserting block $block_data->height ($block_data->hash)... ";
+ $curr_block_entity = $this->Blocks->newEntity($curr_block_ins);
+
+ $conn->execute('INSERT INTO Blocks (Bits, Chainwork, Confirmations, Difficulty, Hash, Height, MedianTime, MerkleRoot, NameClaimRoot, Nonce, PreviousBlockHash, NextBlockHash, BlockSize, Target, BlockTime, TransactionHashes, Version, VersionHex, Created, Modified) ' .
+ 'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP(), UTC_TIMESTAMP())',
+ [
+ $curr_block_entity->Bits,
+ $curr_block_entity->Chainwork,
+ $curr_block_entity->Confirmations,
+ $curr_block_ins['Difficulty'],
+ $curr_block_entity->Hash,
+ $curr_block_entity->Height,
+ $curr_block_entity->MedianTime,
+ $curr_block_entity->MerkleRoot,
+ $curr_block_entity->NameClaimRoot,
+ $curr_block_entity->Nonce,
+ $curr_block_entity->PreviousBlockHash,
+ $curr_block_entity->NextBlockHash,
+ $curr_block_entity->BlockSize,
+ $curr_block_entity->Target,
+ $curr_block_entity->BlockTime,
+ $curr_block_entity->TransactionHashes,
+ $curr_block_entity->Version,
+ $curr_block_entity->VersionHex,
+ ]);
+
+ $stmt = $conn->execute('SELECT LAST_INSERT_ID() AS lBlockId');
+ $linsert = $stmt->fetch(\PDO::FETCH_OBJ);
+ $curr_block_entity->Id = $linsert->lBlockId;
+ $block_id = $curr_block_entity->Id;
+
+ echo "Done.\n";
+ } else {
+ echo "Updating block $block_data->height ($block_data->hash) with next block hash: " . $curr_block_ins['NextBlockHash'] . " and confirmations: " . $curr_block_ins['Confirmations'] . "... ";
+ $upd_block = ['Id' => $old_block->Id, 'NextBlockHash' => $curr_block_ins['NextBlockHash'], 'Confirmations' => $curr_block_ins['Confirmations']];
+ $upd_entity = $this->Blocks->newEntity($upd_block);
+ $block_id = $old_block->Id;
+ echo "Done.\n";
+ }
+
+ $txs = $block_data->tx;
+ $data_error = false;
+ foreach ($txs as $tx_hash) {
+ // Check if the transactions exist and then update the BlockHash and TxTime
+ $tx = $this->Transactions->find()->select(['Id'])->where(['Hash' => $tx_hash])->first();
+ if ($tx) {
+ $upd_tx_data = [
+ 'Id' => $tx->Id,
+ 'BlockHash' => $block_data->hash,
+ 'TransactionTime' => $block_data->time
+ ];
+ $upd_tx_entity = $this->Transactions->newEntity($upd_tx_data);
+ $this->Transactions->save($upd_tx_entity);
+ echo "Updated tx $tx_hash with block hash and time $block_data->time.\n";
+ } else {
+ // Doesn't exist, create a new transaction
+
+ echo "Inserting tx $tx_hash for block height $block_data->height... ";
+
+ $conn->begin();
+ $block_ts = \DateTime::createFromFormat('U', $block_data->time);
+ $this->processtx($tx_hash, $block_ts, ['hash' => $block_data->hash, 'time' => $block_data->time], $data_error);
+
+ if ($data_error) {
+ $conn->rollback();
+ echo "Insert failed.\n";
+ } else {
+ $conn->commit();
+ echo "Done.\n";
+ }
+ }
+ }
+
+ if (!$data_error && $block_id > -1) {
+ // set TransactionsProcessed to true
+ $conn->execute('UPDATE Blocks SET TransactionsProcessed = 1 WHERE Id = ?', [$block_id]);
+ }
+ }
+ }
+ } catch (\Exception $e) {
+ print_r($e);
+ }
+
+ self::unlock('parsenewblocks');
+ }
+
+ public function parsemempool() {
+ self::lock('parsemempool');
+
+ $data = ['method' => 'getrawmempool', 'params' => []];
+ $res = self::curl_json_post(self::rpcurl, json_encode($data));
+ $json = json_decode($res);
+ $txs = $json->result;
+ $now = new \DateTime('now', new \DateTimeZone('UTC'));
+ $data_error = false;
+ $conn = ConnectionManager::get('default');
+
+ foreach ($txs as $tx_hash) {
+ echo "Processing tx hash: $tx_hash... ";
+
+ $exist_tx = $this->Transactions->find()->select(['Id'])->where(['Hash' => $tx_hash])->first();
+ if ($exist_tx) {
+ echo "Exists. Skipping.\n";
+ continue;
+ }
+
+ $conn->begin();
+ $block_ts = new \DateTime('now', new \DateTimeZone('UTC'));
+ $this->processtx($tx_hash, $block_ts, null, $data_error);
+
+ if ($data_error) {
+ echo "Rolling back!\n";
+ $conn->rollback();
+ throw new \Exception('Data save failed!');
+ } else {
+ echo "Data committed.\n";
+ $conn->commit();
+ }
+ }
+
+ self::unlock('parsemempool');
+ }
+
+ public function forevermempool() {
+ self::lock('forevermempool');
+
+ while (true) {
+ try {
+ $data = ['method' => 'getrawmempool', 'params' => []];
+ $res = self::curl_json_post(self::rpcurl, json_encode($data));
+ $json = json_decode($res);
+ $txs = $json->result;
+ $now = new \DateTime('now', new \DateTimeZone('UTC'));
+ $data_error = false;
+ $conn = ConnectionManager::get('default');
+ foreach ($txs as $tx_hash) {
+ echo "Processing tx hash: $tx_hash... ";
+
+ $exist_tx = $this->Transactions->find()->select(['Id'])->where(['Hash' => $tx_hash])->first();
+ if ($exist_tx) {
+ echo "Exists. Skipping.\n";
+ continue;
+ }
+
+ // Process the tx
+ $conn->begin();
+ $block_ts = new \DateTime('now', new \DateTimeZone('UTC'));
+ $this->processtx($tx_hash, $block_ts, null, $data_error);
+
+ if ($data_error) {
+ echo "Rolling back!\n";
+ $conn->rollback();
+ throw new \Exception('Data save failed!');
+ } else {
+ echo "Data committed.\n";
+ $conn->commit();
+ }
+ }
+ } catch (\Exception $e) {
+ echo "Error occurred processing mempool: " . $e->getMessage() . "\n";
+ }
+
+ echo "Sleeping for 15 seconds before next iteration.\n";
+ sleep(15);
+ }
+
+ self::unlock('forevermempool');
+ }
+
+ private function txdb_data_from_decoded($decoded_tx) {
+ $tx = [
+ 'Version' => $decoded_tx['version'],
+ 'LockTime' => $decoded_tx['locktime']
+ ];
+
+ $addresses = [];
+ $inputs = [];
+ $outputs = [];
+
+ $vin = $decoded_tx['vin'];
+ $vout = $decoded_tx['vout'];
+ if (is_array($vin)) {
+ foreach ($vin as $in) {
+ if (isset($in['coinbase'])) {
+ $inputs[] = [
+ 'IsCoinbase' => 1,
+ 'Coinbase' => $in['coinbase']
+ ];
+ } else {
+ $inputs[] = [
+ 'PrevoutHash' => $in['txid'],
+ 'PrevoutN' => $in['vout'],
+ 'ScriptSigAsm' => $in['scriptSig']['asm'],
+ 'ScriptSigHex' => $in['scriptSig']['hex'],
+ 'Sequence' => $in['sequence']
+ ];
+ }
+ }
+
+ foreach ($vout as $out) {
+ $outputs[] = [
+ 'Vout' => $out['vout'],
+ 'Value' => bcdiv($out['value'], 100000000, 8),
+ 'Type' => isset($out['scriptPubKey']['type']) ? $out['scriptPubKey']['type'] : '',
+ 'ScriptPubKeyAsm' => isset($out['scriptPubKey']['asm']) ? $out['scriptPubKey']['asm'] : '',
+ 'ScriptPubKeyHex' => isset($out['scriptPubKey']['hex']) ? $out['scriptPubKey']['hex'] : '',
+ 'RequiredSignatures' => isset($out['scriptPubKey']['reqSigs']) ? $out['scriptPubKey']['reqSigs'] : '',
+ 'Hash160' => isset($out['scriptPubKey']['hash160']) ? $out['scriptPubKey']['hash160'] : '',
+ 'Addresses' => isset($out['scriptPubKey']['addresses']) ? json_encode($out['scriptPubKey']['addresses']) : null
+ ];
+
+ if (isset($out['scriptPubKey']['addresses'])) {
+ foreach ($out['scriptPubKey']['addresses'] as $address) {
+ $addresses[$address] = $address;
+ }
+ }
+ }
+ }
+
+ return ['tx' => $tx, 'addresses' => $addresses, 'inputs' => $inputs, 'outputs' => $outputs];
+ }
+
+ private function blockdb_data_from_json($json_block) {
+ return [
+ 'Bits' => $json_block->bits,
+ 'Chainwork' => $json_block->chainwork,
+ 'Confirmations' => $json_block->confirmations,
+ 'Difficulty' => $json_block->difficulty,
+ 'Hash' => $json_block->hash,
+ 'Height' => $json_block->height,
+ 'MedianTime' => $json_block->mediantime,
+ 'MerkleRoot' => $json_block->merkleroot,
+ 'NameClaimRoot' => $json_block->nameclaimroot,
+ 'Nonce' => $json_block->nonce,
+ 'PreviousBlockHash' => isset($json_block->previousblockhash) ? $json_block->previousblockhash : null,
+ 'NextBlockHash' => isset($json_block->nextblockhash) ? $json_block->nextblockhash : null,
+ 'BlockSize' => $json_block->size,
+ 'Target' => $json_block->target,
+ 'BlockTime' => $json_block->time,
+ 'TransactionHashes' => json_encode($json_block->tx),
+ 'Version' => $json_block->version,
+ 'VersionHex' => $json_block->versionHex
+ ];
+ }
+
+ private static function curl_json_post($url, $data, $headers = []) {
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+
+ $response = curl_exec($ch);
+ //Log::debug('Request execution completed.');
+ if ($response === false) {
+ $error = curl_error($ch);
+ $errno = curl_errno($ch);
+ curl_close($ch);
+
+ throw new \Exception(sprintf('The request failed: %s', $error), $errno);
+ } else {
+ curl_close($ch);
+ }
+
+ // Close any open file handle
+ return $response;
+ }
+
+ private static $base58chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
+
+ public static $op_codes = [
+ ['OP_0', 0],
+ ['OP_PUSHDATA', 76],
+ 'OP_PUSHDATA2',
+ 'OP_PUSHDATA4',
+ 'OP_1NEGATE',
+ 'OP_RESERVED',
+ 'OP_1',
+ 'OP_2',
+ 'OP_3',
+ 'OP_4',
+ 'OP_5',
+ 'OP_6',
+ 'OP_7',
+ 'OP_8', 'OP_9', 'OP_10', 'OP_11', 'OP_12', 'OP_13', 'OP_14', 'OP_15', 'OP_16',
+ 'OP_NOP', 'OP_VER', 'OP_IF', 'OP_NOTIF', 'OP_VERIF', 'OP_VERNOTIF', 'OP_ELSE', 'OP_ENDIF', 'OP_VERIFY',
+ 'OP_RETURN', 'OP_TOALTSTACK', 'OP_FROMALTSTACK', 'OP_2DROP', 'OP_2DUP', 'OP_3DUP', 'OP_2OVER', 'OP_2ROT', 'OP_2SWAP',
+ 'OP_IFDUP', 'OP_DEPTH', 'OP_DROP', 'OP_DUP', 'OP_NIP', 'OP_OVER', 'OP_PICK', 'OP_ROLL', 'OP_ROT',
+ 'OP_SWAP', 'OP_TUCK', 'OP_CAT', 'OP_SUBSTR', 'OP_LEFT', 'OP_RIGHT', 'OP_SIZE', 'OP_INVERT', 'OP_AND',
+ 'OP_OR', 'OP_XOR', 'OP_EQUAL', 'OP_EQUALVERIFY', 'OP_RESERVED1', 'OP_RESERVED2', 'OP_1ADD', 'OP_1SUB', 'OP_2MUL',
+ 'OP_2DIV', 'OP_NEGATE', 'OP_ABS', 'OP_NOT', 'OP_0NOTEQUAL', 'OP_ADD', 'OP_SUB', 'OP_MUL', 'OP_DIV',
+ 'OP_MOD', 'OP_LSHIFT', 'OP_RSHIFT', 'OP_BOOLAND', 'OP_BOOLOR',
+ 'OP_NUMEQUAL', 'OP_NUMEQUALVERIFY', 'OP_NUMNOTEQUAL', 'OP_LESSTHAN',
+ 'OP_GREATERTHAN', 'OP_LESSTHANOREQUAL', 'OP_GREATERTHANOREQUAL', 'OP_MIN', 'OP_MAX',
+ 'OP_WITHIN', 'OP_RIPEMD160', 'OP_SHA1', 'OP_SHA256', 'OP_HASH160',
+ 'OP_HASH256', 'OP_CODESEPARATOR', 'OP_CHECKSIG', 'OP_CHECKSIGVERIFY', 'OP_CHECKMULTISIG',
+ 'OP_CHECKMULTISIGVERIFY', 'OP_NOP1', 'OP_NOP2', 'OP_NOP3', 'OP_NOP4', 'OP_NOP5', 'OP_CLAIM_NAME',
+ 'OP_SUPPORT_CLAIM', 'OP_UPDATE_CLAIM',
+ ['OP_SINGLEBYTE_END', 0xF0],
+ ['OP_DOUBLEBYTE_BEGIN', 0xF000],
+ 'OP_PUBKEY', 'OP_PUBKEYHASH',
+ ['OP_INVALIDOPCODE', 0xFFFF]
+ ];
+
+ public static $op_code = array(
+ '00' => 'OP_0', // or OP_FALSE
+ '51' => 'OP_1', // or OP_TRUE
+ '61' => 'OP_NOP',
+ '6a' => 'OP_RETURN',
+ '6d' => 'OP_2DROP',
+ '75' => 'OP_DROP',
+ '76' => 'OP_DUP',
+ '87' => 'OP_EQUAL',
+ '88' => 'OP_EQUALVERIFY',
+ 'a6' => 'OP_RIPEMD160',
+ 'a7' => 'OP_SHA1',
+ 'a8' => 'OP_SHA256',
+ 'a9' => 'OP_HASH160',
+ 'aa' => 'OP_HASH256',
+ 'ac' => 'OP_CHECKSIG',
+ 'ae' => 'OP_CHECKMULTISIG',
+ 'b5' => 'OP_CLAIM_NAME',
+ 'b6' => 'OP_SUPPORT_CLAIM',
+ 'b7' => 'OP_UPDATE_CLAIM'
+ );
+
+ /*protected static function hash160_to_bc_address($h160, $addrType = 0) {
+ $vh160 = $c . $h160;
+ $h = self::_dhash($vh160);
+
+ $addr = $vh160 . substr($h, 0, 4);
+ return $addr;
+ //return self::base58_encode($addr);
+ }*/
+
+ protected static function _dhash($str, $raw = false) {
+ return hash('sha256', hash('sha256', $str, true), $raw);
+ }
+
+ public static function _get_vint(&$string)
+ {
+ // Load the next byte, convert to decimal.
+ $decimal = hexdec(self::_return_bytes($string, 1));
+ // Less than 253: Not encoding extra bytes.
+ // More than 253, work out the $number of bytes using the 2^(offset)
+ $num_bytes = ($decimal < 253) ? 0 : 2 ^ ($decimal - 253);
+ // Num_bytes is 0: Just return the decimal
+ // Otherwise, return $num_bytes bytes (order flipped) and converted to decimal
+ return ($num_bytes == 0) ? $decimal : hexdec(self::_return_bytes($string, $num_bytes, true));
+ }
+
+ public static function hash160($string)
+ {
+ $bs = @pack("H*", $string);
+ return hash("ripemd160", hash("sha256", $bs, true));
+ }
+
+ public static function hash256($string)
+ {
+ $bs = @pack("H*", $string);
+ return hash("sha256", hash("sha256", $bs, true));
+ }
+
+ public static function hash160_to_address($hash160, $address_version = null)
+ {
+ $c = '';
+ if ($address_version == self::pubKeyAddress[0]) {
+ $c = dechex(self::pubKeyAddress[1]);
+ } else if ($address_version == self::scriptAddress[0]) {
+ $c = dechex(self::scriptAddress[1]);
+ }
+
+ $hash160 = $c . $hash160;
+ $addr = $hash160;
+ return self::base58_encode_checksum($addr);
+ }
+
+ public static function base58_encode_checksum($hex)
+ {
+ $checksum = self::hash256($hex);
+ $checksum = substr($checksum, 0, 8);
+ $hash = $hex . $checksum;
+ return self::base58_encode($hash);
+ }
+
+ public static function _decode_script($script)
+ {
+ $pos = 0;
+ $data = array();
+ while ($pos < strlen($script)) {
+ $code = hexdec(substr($script, $pos, 2)); // hex opcode.
+ $pos += 2;
+ if ($code < 1) {
+ // OP_FALSE
+ $push = '0';
+ } elseif ($code <= 75) {
+ // $code bytes will be pushed to the stack.
+ $push = substr($script, $pos, ($code * 2));
+ $pos += $code * 2;
+ } elseif ($code <= 78) {
+ // In this range, 2^($code-76) is the number of bytes to take for the *next* number onto the stack.
+ $szsz = pow(2, $code - 75); // decimal number of bytes.
+ $sz = hexdec(substr($script, $pos, ($szsz * 2))); // decimal number of bytes to load and push.
+ $pos += $szsz;
+ $push = substr($script, $pos, ($pos + $sz * 2)); // Load the data starting from the new position.
+ $pos += $sz * 2;
+ } elseif ($code <= 108/*96*/) {
+ // OP_x, where x = $code-80
+ $push = ($code - 80);
+ } else {
+ $push = $code;
+ }
+ $data[] = $push;
+ }
+
+ return implode(" ", $data);
+ }
+
+ /*public static function script_getopname($bytes_hex) {
+ $index = hexdec($bytes_hex) - 75;
+ $op = self::$op_codes[$index];
+ if (is_array($op)) {
+ if ($bytes_hex == dechex($op[1])) {
+ return $op[0];
+ }
+ }
+ if ()
+
+ return str_replace('OP_', '', $op);
+ }*/
+
+ public static function get_opcode($opname) {
+ $len = count(self::$op_codes);
+ for ($i = 0; $i < $len; $i++) {
+ $op = self::$op_codes[$i];
+ $op = is_array($op) ? $op[0] : $op;
+ if ($op === $opname) {
+ return $i;
+ }
+ }
+
+ return null;
+ }
+
+ public static function match_decoded($decoded, $to_match) {
+ if (strlen($decoded) != strlen($to_match)) {
+ return false;
+ }
+
+ for ($i = 0; $i < count($decoded); $i++) {
+ $pushdata4 = self::get_opcode('OP_PUSHDATA4');
+ if ($to_match[$i] == $pushdata4 && ($pushdata4 >= $decoded[$i][0]) > 0) {
+ continue;
+ }
+ if ($to_match[$i] != $decoded[$i][0]) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public static function base58_encode($hex)
+ {
+ if (strlen($hex) == 0) {
+ return '';
+ }
+ // Convert the hex string to a base10 integer
+ $num = gmp_strval(gmp_init($hex, 16), 58);
+ // Check that number isn't just 0 - which would be all padding.
+ if ($num != '0') {
+ $num = strtr($num, '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuv', self::$base58chars);
+ } else {
+ $num = '';
+ }
+ // Pad the leading 1's
+ $pad = '';
+ $n = 0;
+ while (substr($hex, $n, 2) == '00') {
+ $pad .= '1';
+ $n += 2;
+ }
+ return $pad . $num;
+ }
+
+ public static function decode_tx($raw_transaction)
+ {
+ $math = EccFactory::getAdapter();
+ /*$magic_byte = BitcoinLib::magicByte($magic_byte);
+ $magic_p2sh_byte = BitcoinLib::magicP2SHByte($magic_p2sh_byte);*/
+ $raw_transaction = trim($raw_transaction);
+ if (((bool)preg_match('/^[0-9a-fA-F]{2,}$/i', $raw_transaction) !== true)
+ || (strlen($raw_transaction)) % 2 !== 0
+ ) {
+ throw new \InvalidArgumentException("Raw transaction is invalid hex");
+ }
+ $txHash = hash('sha256', hash('sha256', pack("H*", trim($raw_transaction)), true));
+ $txid = self::_flip_byte_order($txHash);
+ $info = array();
+ $info['txid'] = $txid;
+ $info['version'] = $math->hexDec(self::_return_bytes($raw_transaction, 4, true));
+ /*if (!in_array($info['version'], array('0', '1'))) {
+ throw new \InvalidArgumentException("Invalid transaction version");
+ }*/
+ $input_count = self::_get_vint($raw_transaction);
+ if (!($input_count >= 0 && $input_count <= 4294967296)) {
+ throw new \InvalidArgumentException("Invalid input count");
+ }
+ $info['vin'] = self::_decode_inputs($raw_transaction, $input_count);
+ if ($info['vin'] == false) {
+ throw new \InvalidArgumentException("No inputs in transaction");
+ }
+ $output_count = self::_get_vint($raw_transaction);
+ if (!($output_count >= 0 && $output_count <= 4294967296)) {
+ throw new \InvalidArgumentException("Invalid output count");
+ }
+ $info['vout'] = self::_decode_outputs($raw_transaction, $output_count);
+ $info['locktime'] = $math->hexDec(self::_return_bytes($raw_transaction, 4));
+ return $info;
+ }
+
+ public static function _decode_inputs(&$raw_transaction, $input_count)
+ {
+ $inputs = array();
+ // Loop until $input count is reached, sequentially removing the
+ // leading data from $raw_transaction reference.
+ for ($i = 0; $i < $input_count; $i++) {
+ // Load the TxID (32bytes) and vout (4bytes)
+ $txid = self::_return_bytes($raw_transaction, 32, true);
+ $vout = self::_return_bytes($raw_transaction, 4, true);
+ // Script is prefixed with a varint that must be decoded.
+ $script_length = self::_get_vint($raw_transaction); // decimal number of bytes.
+ $script = self::_return_bytes($raw_transaction, $script_length);
+
+ // Build input body depending on whether the TxIn is coinbase.
+ if ($txid == '0000000000000000000000000000000000000000000000000000000000000000') {
+ $input_body = array('coinbase' => $script);
+ } else {
+ $input_body = array('txid' => $txid,
+ 'vout' => hexdec($vout),
+ 'scriptSig' => array('asm' => self::_decode_script($script), 'hex' => $script));
+ }
+ // Append a sequence number, and finally add the input to the array.
+ $input_body['sequence'] = hexdec(self::_return_bytes($raw_transaction, 4));
+ $inputs[$i] = $input_body;
+ }
+ return $inputs;
+ }
+
+ public static function _decode_outputs(&$tx, $output_count)
+ {
+ $math = EccFactory::getAdapter();
+ /*$magic_byte = BitcoinLib::magicByte($magic_byte);
+ $magic_p2sh_byte = BitcoinLib::magicP2SHByte($magic_p2sh_byte);*/
+ $outputs = array();
+ for ($i = 0; $i < $output_count; $i++) {
+ // Pop 8 bytes (flipped) from the $tx string, convert to decimal,
+ // and then convert to Satoshis.
+ $satoshis = $math->hexDec(self::_return_bytes($tx, 8, true));
+ // Decode the varint for the length of the scriptPubKey
+ $script_length = self::_get_vint($tx); // decimal number of bytes
+ $script = self::_return_bytes($tx, $script_length);
+
+
+ try {
+ $asm = self::_decode_scriptPubKey($script);
+ } catch (\Exception $e) {
+ $asm = null;
+ }
+ // Begin building scriptPubKey
+ $scriptPubKey = array(
+ 'asm' => $asm,
+ 'hex' => $script
+ );
+
+ // Try to decode the scriptPubKey['asm'] to learn the transaction type.
+ $txn_info = self::_get_transaction_type($scriptPubKey['asm']);
+ if ($txn_info !== false) {
+ $scriptPubKey = array_merge($scriptPubKey, $txn_info);
+ }
+ $outputs[$i] = array(
+ 'value' => $satoshis,
+ 'vout' => $i,
+ 'scriptPubKey' => $scriptPubKey);
+ }
+ return $outputs;
+ }
+
+ public function fixoutputs() {
+ $sql = 'SELECT * FROM Outputs WHERE Id NOT IN (SELECT OutputId FROM OutputsAddresses)';
+
+ $conn = ConnectionManager::get('default');
+ $stmt = $conn->execute($sql);
+ $outs = $stmt->fetchAll(\PDO::FETCH_OBJ);
+
+ foreach ($outs as $out) {
+ $txn_info = self::_get_transaction_type($out->ScriptPubKeyAsm);
+
+ $out_data = [
+ 'Id' => $out->Id,
+ 'Type' => $txn_info['type'],
+ 'RequiredSignatures' => $txn_info['reqSigs'],
+ 'Hash160' => $txn_info['hash160'],
+ 'Addresses' => json_encode($txn_info['addresses'])
+ ];
+ $out_entity = $this->Outputs->newEntity($out_data);
+ $this->Outputs->save($out_entity);
+
+ // Fix the addresses
+ foreach ($txn_info['addresses'] as $address) {
+ $prev_addr = $this->Addresses->find()->where(['Address' => $address])->first();
+ $addr_id = -1;
+ if ($prev_addr) {
+ $addr_id = $prev_addr->Id;
+ } else {
+ $dt = new \DateTime($out->Created, new \DateTimeZone('UTC'));
+ $new_addr = [
+ 'Address' => $address,
+ 'FirstSeen' => $dt->format('Y-m-d H:i:s')
+ ];
+ $new_addr_entity = $this->Addresses->newEntity($new_addr);
+ if ($this->Addresses->save($new_addr_entity)) {
+ $addr_id = $new_addr_entity->Id;
+ }
+ }
+
+ if ($addr_id > -1) {
+ $conn->execute('REPLACE INTO OutputsAddresses (OutputId, AddressId) VALUES (?, ?)', [$out->Id, $addr_id]);
+ }
+ }
+
+ echo "Fixed output $out->Id with new data: " . print_r($out_data, true);
+ }
+ }
+
+ public function fixinputs() {
+ $sql = 'SELECT * FROM Inputs WHERE IsCoinbase <> 1 AND Id NOT IN (SELECT InputId FROM InputsAddresses)';
+
+ $conn = ConnectionManager::get('default');
+ $stmt = $conn->execute($sql);
+ $ins = $stmt->fetchAll(\PDO::FETCH_OBJ);
+
+ foreach ($ins as $in) {
+ $prev_tx_hash = $in->PrevoutHash;
+ $prev_n = $in->PrevoutN;
+
+ // Get the previous transaction
+ $prev_tx = $this->Transactions->find()->select(['Id'])->where(['Hash' => $prev_tx_hash])->first();
+ if (!$prev_tx) {
+ echo "Previous tx for hash $prev_tx_hash not found.\n";
+ continue;
+ }
+
+ $prev_tx_id = $prev_tx->Id;
+ $src_output = $this->Outputs->find()->contain(['OutputAddresses'])->where(['TransactionId' => $prev_tx_id, 'Vout' => $prev_n])->first();
+ $in_data = ['Id' => $in->Id];
+ if ($src_output) {
+ $in_data['Value'] = $src_output->Value;
+ $in_data['AddressId'] = $src_output->OutputAddresses[0]->Id;
+
+ $in_entity = $this->Inputs->newEntity($in_data);
+ if ($this->Inputs->save($in_entity)) {
+ $conn->execute('REPLACE INTO InputsAddresses (InputId, AddressId) VALUES (?, ?)', [$in->Id, $in_data['AddressId']]);
+ }
+ }
+
+ echo "Fixed input $in->Id with new data: " . print_r($in_data, true);
+ }
+ }
+
+ public static function _get_transaction_type($data)
+ {
+ //$magic_byte = BitcoinLib::magicByte($magic_byte);
+ //$magic_p2sh_byte = BitcoinLib::magicP2SHByte($magic_p2sh_byte);
+ $has_claim = (strpos($data, 'CLAIM') !== false);
+ $has_update_claim = (strpos($data, 'UPDATE_CLAIM') !== false);
+ $has_op_0 = (strpos($data, 'OP_0') !== false);
+ $data = explode(" ", trim($data));
+ // Define information about eventual transactions cases, and
+ // the position of the hash160 address in the stack.
+ $define = array();
+ $rule = array();
+
+ // Other standard: pay to pubkey hash
+ $define['p2pk'] = array('type' => 'pubkeyhash',
+ 'reqSigs' => 1,
+ 'data_index_for_hash' => 1);
+ $rule['p2pk'] = [
+ '0' => '/^[0-9a-f]+$/i',
+ '1' => '/^OP_CHECKSIG/'
+ ];
+
+ // Pay to script hash
+ $define['p2sh'] = array('type' => 'scripthash',
+ 'reqSigs' => 1,
+ 'data_index_for_hash' => 1);
+ $rule['p2sh'] = array(
+ '0' => '/^OP_HASH160/',
+ '1' => '/^[0-9a-f]{40}$/i', // pos 1
+ '2' => '/^OP_EQUAL/');
+
+ // Non-standard (claim_name and support_claim)
+ $define['p2c'] = array('type' => 'nonstandard',
+ 'reqSigs' => 1,
+ 'data_index_for_hash' => 7);
+ $rule['p2c'] = [
+ '0' => '/^OP_CLAIM_NAME|OP_SUPPORT_CLAIM/',
+ '1' => '/^[0-9a-f]+$/i',
+ '2' => '/^[0-9a-f]+$/i',
+ '3' => '/^OP_2DROP/',
+ '4' => '/^OP_DROP/',
+ '5' => '/^OP_DUP/',
+ '6' => '/^OP_HASH160/',
+ '7' => '/^[0-9a-f]{40}$/i', // pos 7
+ '8' => '/^OP_EQUALVERIFY/',
+ '9' => '/^OP_CHECKSIG/',
+ ];
+
+ // Non-standard (claim_name and support_claim)
+ $define['p2c2'] = array('type' => 'nonstandard',
+ 'reqSigs' => 1,
+ 'data_index_for_hash' => 7);
+ $rule['p2c2'] = [
+ '0' => '/^OP_CLAIM_NAME|OP_SUPPORT_CLAIM/',
+ '1' => '/^OP_0/',
+ '2' => '/^[0-9a-f]+$/i',
+ '3' => '/^OP_2DROP/',
+ '4' => '/^OP_DROP/',
+ '5' => '/^OP_DUP/',
+ '6' => '/^OP_HASH160/',
+ '7' => '/^[0-9a-f]{40}$/i', // pos 8
+ '8' => '/^OP_EQUALVERIFY/',
+ '9' => '/^OP_CHECKSIG/',
+ ];
+
+ // update_claim
+ $define['p2uc'] = array('type' => 'nonstandard',
+ 'reqSigs' => 1,
+ 'data_index_for_hash' => 8);
+ $rule['p2uc'] = [
+ '0' => '/^OP_UPDATE_CLAIM/',
+ '1' => '/^[0-9a-f]+$/i',
+ '2' => '/^[0-9a-f]+$/i',
+ '3' => '/^[0-9a-f]+$/i',
+ '4' => '/^OP_2DROP/',
+ '5' => '/^OP_2DROP/',
+ '6' => '/^OP_DUP/',
+ '7' => '/^OP_HASH160/',
+ '8' => '/^[0-9a-f]{40}$/i', // pos 8
+ '9' => '/^OP_EQUALVERIFY/',
+ '10' => '/^OP_CHECKSIG/',
+ ];
+
+ // Standard: pay to pubkey hash
+ $define['p2ph'] = array('type' => 'pubkeyhash',
+ 'reqSigs' => 1,
+ 'data_index_for_hash' => 2);
+ $rule['p2ph'] = array(
+ '0' => '/^OP_DUP/',
+ '1' => '/^OP_HASH160/',
+ '2' => '/^[0-9a-f]{40}$/i', // 2
+ '3' => '/^OP_EQUALVERIFY/',
+ '4' => '/^OP_CHECKSIG/');
+
+ if ($has_claim) {
+ unset($rule['p2ph']);
+ unset($rule['p2sh']);
+
+ if ($has_op_0) {
+ unset($rule['p2c']);
+ } else {
+ unset($rule['p2c2']);
+ }
+
+ if ($has_update_claim) {
+ unset($rule['p2c']);
+ } else {
+ unset($rule['p2uc']);
+ }
+ } else {
+ unset($rule['p2c']);
+ unset($rule['p2c2']);
+ unset($rule['p2uc']);
+ }
+
+ // Work out how many rules are applied in each case
+ $valid = array();
+ foreach ($rule as $tx_type => $def) {
+ $valid[$tx_type] = count($def);
+ }
+
+ // Attempt to validate against each of these rules.
+ $matches = [];
+ for ($index = 0; $index < count($data); $index++) {
+ $test = $data[$index];
+ foreach ($rule as $tx_type => $def) {
+ if (isset($def[$index])) {
+ preg_match($def[$index], $test, $matches[$tx_type]);
+ if (count($matches[$tx_type]) == 1) {
+ $valid[$tx_type]--;
+ break;
+ }
+ }
+ }
+ }
+
+ // Loop through rules, check if any transaction is a match.
+ foreach ($rule as $tx_type => $def) {
+ if ($valid[$tx_type] == 0) {
+ // Load predefined info for this transaction type if detected.
+ $return = $define[$tx_type];
+ if ($tx_type === 'p2pk') {
+ $return['hash160'] = self::hash160($data[$define[$tx_type]['data_index_for_hash']]);
+ $return['addresses'][0] = self::hash160_to_address($return['hash160'], self::pubKeyAddress[0]);
+ } else {
+ $return['hash160'] = $data[$define[$tx_type]['data_index_for_hash']];
+ $return['addresses'][0] = self::hash160_to_address($return['hash160'], self::pubKeyAddress[0]); // TODO: Pay to claim transaction?
+ }
+ unset($return['data_index_for_hash']);
+ }
+ }
+ return (!isset($return)) ? false : $return;
+ }
+
+ public static function _decode_scriptPubKey($script, $matchBitcoinCore = false)
+ {
+ $data = array();
+ while (strlen($script) !== 0) {
+ $byteHex = self::_return_bytes($script, 1);
+ $byteInt = hexdec($byteHex);
+
+ if (isset(self::$op_code[$byteHex])) {
+ // This checks if the OPCODE is defined from the list of constants.
+ if ($matchBitcoinCore && self::$op_code[$byteHex] == "OP_0") {
+ $data[] = '0';
+ } else if ($matchBitcoinCore && self::$op_code[$byteHex] == "OP_1") {
+ $data[] = '1';
+ } else {
+ $data[] = self::$op_code[$byteHex];
+ }
+ } elseif ($byteInt >= 0x01 && $byteInt <= 0x4e) {
+ // This checks if the OPCODE falls in the PUSHDATA range
+ if ($byteInt == 0x4d) {
+ // OP_PUSHDATA2
+ $byteInt = hexdec(self::_return_bytes($script, 2, true));
+ $data[] = self::_return_bytes($script, $byteInt);
+ } else if ($byteInt == 0x4e) {
+ // OP_PUSHDATA4
+ $byteInt = hexdec(self::_return_bytes($script, 4, true));
+ $data[] = self::_return_bytes($script, $byteInt);
+ } else if ($byteInt == 0x4c) {
+ $num_bytes = hexdec(self::_return_bytes($script, 1, true));
+ $data[] = self::_return_bytes($script, $num_bytes);
+ } else {
+ $data[] = self::_return_bytes($script, $byteInt);
+ }
+ } elseif ($byteInt >= 0x51 && $byteInt <= 0x60) {
+ // This checks if the CODE falls in the OP_X range
+ $data[] = $matchBitcoinCore ? ($byteInt - 0x50) : 'OP_' . ($byteInt - 0x50);
+ } else {
+ throw new \RuntimeException("Failed to decode scriptPubKey");
+ }
+ }
+
+ return implode(" ", $data);
+ }
+
+ public static function lock($process_name) {
+ if (!is_dir(TMP . 'lock')) {
+ mkdir(TMP . 'lock');
+ }
+ $lock_file = TMP . 'lock' . DS . $process_name;
+ if (file_exists($lock_file)) {
+ echo "$process_name is already running.\n";
+ exit(0);
+ }
+ file_put_contents($lock_file, '1');
+ }
+
+ public static function unlock($process_name) {
+ $lock_file = TMP . 'lock' . DS . $process_name;
+ if (file_exists($lock_file)) {
+ unlink($lock_file);
+ }
+ return true;
+ }
+
+ public static function _return_bytes(&$string, $byte_count, $reverse = false)
+ {
+ if (strlen($string) < $byte_count * 2) {
+ throw new \InvalidArgumentException("Could not read enough bytes");
+ }
+ $requested_bytes = substr($string, 0, $byte_count * 2);
+ // Overwrite $string, starting $byte_count bytes from the start.
+ $string = substr($string, $byte_count * 2);
+ // Flip byte order if requested.
+ return ($reverse == false) ? $requested_bytes : self::_flip_byte_order($requested_bytes);
+ }
+
+ public static function _flip_byte_order($bytes) {
+ return implode('', array_reverse(str_split($bytes, 2)));
+ }
+
+ /*public function parsehistoryblocks() {
+ set_time_limit(0);
+ header('Content-type: text/plain');
+
+ $block_hash = null;
+ // Get the minimum block hash first
+ $minBlock = $this->Blocks->find()->select(['Hash'])->order(['Height' => 'asc'])->first();
+ if (!$minBlock) {
+ // get the best block
+ $req = ['method' => 'status'];
+ $response = self::curl_json_post(self::lbryurl, json_encode($req));
+ $json = json_decode($response);
+ $block_hash = $json->result->blockchain_status->best_blockhash;
+ } else {
+ $block_hash = $minBlock->Hash;
+ }
+
+ echo "Processing block: $block_hash... ";
+ $req = ['method' => 'block_show', 'params' => ['blockhash' => $block_hash]];
+ $response = self::curl_json_post(self::lbryurl, json_encode($req));
+ $json = json_decode($response);
+ $block_data = $json->result;
+
+ // Check if the block exists
+ $oldBlock = $this->Blocks->find()->select(['Id'])->where(['Hash' => $block_hash])->first();
+ if (!$oldBlock) {
+ // Block does not exist, create the block
+ $newBlock = $this->blockdb_data_from_json($block_data);
+ $entity = $this->Blocks->newEntity($newBlock);
+ $this->Blocks->save($entity);
+ }
+ echo "Done.\n";
+
+ $prevBlockHash = isset($block_data->previousblockhash) ? $block_data->previousblockhash : null;
+ do {
+ $oldBlock = $this->Blocks->find()->select(['Id'])->where(['Hash' => $prevBlockHash])->first();
+ $req = ['method' => 'block_show', 'params' => ['blockhash' => $prevBlockHash]];
+ $response = self::curl_json_post(self::lbryurl, json_encode($req));
+ $json = json_decode($response);
+ $block_data = $json->result;
+ $prevBlockHash = isset($block_data->previousblockhash) ? $block_data->previousblockhash : null;
+
+ if (!$oldBlock) {
+ echo "Inserting block: $block_data->hash... ";
+ $newBlock = $this->blockdb_data_from_json($block_data);
+ $entity = $this->Blocks->newEntity($newBlock);
+ $this->Blocks->save($entity);
+ } else {
+ echo "Updating block: $block_data->hash with confirmations: $block_data->confirmations... ";
+ $updData = ['Id' => $oldBlock->Id, 'Confirmations' => $block_data->confirmations];
+ $entity = $this->Blocks->newEntity($newBlock);
+ $this->Blocks->save($entity);
+ }
+ echo "Done.\n";
+ } while($prevBlockHash != null && strlen(trim($prevBlockHash)) > 0);
+
+ exit(0);
+ }
+
+ public function updatespends() {
+ set_time_limit(0);
+
+ self::lock('updatespends');
+
+ try {
+ $conn = ConnectionManager::get('default');
+ $inputs = $this->Inputs->find()->select(['Id', 'PrevoutHash', 'PrevoutN'])->where(['PrevoutSpendUpdated' => 0, 'IsCoinbase <>' => 1])->limit(500000)->toArray();
+
+ $count = count($inputs);
+ $idx = 0;
+ echo sprintf("Processing %d inputs.\n", $count);
+ foreach ($inputs as $in) {
+ $idx++;
+ $idx_str = str_pad($idx, strlen($count), '0', STR_PAD_LEFT);
+
+ $tx = $this->Transactions->find()->select(['Id'])->where(['Hash' => $in->PrevoutHash])->first();
+ if ($tx) {
+ $data_error = false;
+
+ $conn->begin();
+
+ try {
+ // update the corresponding output and set it as spent
+ $conn->execute('UPDATE Outputs SET IsSpent = 1, SpentByInputId = ?, Modified = UTC_TIMESTAMP() WHERE TransactionId = ? AND Vout = ?', [$in->Id, $tx->Id, $in->PrevoutN]);
+ } catch (\Exception $e) {
+ $data_error = true;
+ }
+
+ if (!$data_error) {
+ // update the input
+ $in_data = ['Id' => $in->Id, 'PrevoutSpendUpdated' => 1];
+ $in_entity = $this->Inputs->newEntity($in_data);
+ $result = $this->Inputs->save($in_entity);
+
+ if (!$result) {
+ $data_error = true;
+ }
+ }
+
+ if ($data_error) {
+ echo sprintf("[$idx_str/$count] Could NOT update vout %s for transaction hash %s.\n", $in->PrevoutN, $in->PrevoutHash);
+ $conn->rollback();
+ } else {
+ echo sprintf("[$idx_str/$count] Updated vout %s for transaction hash %s.\n", $in->PrevoutN, $in->PrevoutHash);
+ $conn->commit();
+ }
+ } else {
+ echo sprintf("[$idx_str/$count] Transaction NOT found for tx hash %s.\n", $in->PrevoutHash);
+ }
+ }
+ } catch (\Exception $e) {
+ print_r($e);
+ }
+
+ self::unlock('updatespends');
+ }
+ */
+}
+
+?>
\ No newline at end of file
diff --git a/src/Shell/ConsoleShell.php b/src/Shell/ConsoleShell.php
new file mode 100644
index 0000000..c84bd97
--- /dev/null
+++ b/src/Shell/ConsoleShell.php
@@ -0,0 +1,81 @@
+err('Unable to load Psy\Shell. ');
+ $this->err('');
+ $this->err('Make sure you have installed psysh as a dependency,');
+ $this->err('and that Psy\Shell is registered in your autoloader.');
+ $this->err('');
+ $this->err('If you are using composer run');
+ $this->err('');
+ $this->err('$ php composer.phar require --dev psy/psysh ');
+ $this->err('');
+
+ return self::CODE_ERROR;
+ }
+
+ $this->out("You can exit with `CTRL-C` or `exit` ");
+ $this->out('');
+
+ Log::drop('debug');
+ Log::drop('error');
+ $this->_io->setLoggers(false);
+ restore_error_handler();
+ restore_exception_handler();
+
+ $psy = new PsyShell();
+ $psy->run();
+ }
+
+ /**
+ * Display help for this console.
+ *
+ * @return \Cake\Console\ConsoleOptionParser
+ */
+ public function getOptionParser()
+ {
+ $parser = new ConsoleOptionParser('console');
+ $parser->setDescription(
+ 'This shell provides a REPL that you can use to interact ' .
+ 'with your application in an interactive fashion. You can use ' .
+ 'it to run adhoc queries with your models, or experiment ' .
+ 'and explore the features of CakePHP and your application.' .
+ "\n\n" .
+ 'You will need to have psysh installed for this Shell to work.'
+ );
+
+ return $parser;
+ }
+}
diff --git a/src/Template/Element/Flash/default.ctp b/src/Template/Element/Flash/default.ctp
new file mode 100644
index 0000000..736b27d
--- /dev/null
+++ b/src/Template/Element/Flash/default.ctp
@@ -0,0 +1,10 @@
+
+= $message ?>
diff --git a/src/Template/Element/Flash/error.ctp b/src/Template/Element/Flash/error.ctp
new file mode 100644
index 0000000..e7c4af1
--- /dev/null
+++ b/src/Template/Element/Flash/error.ctp
@@ -0,0 +1,6 @@
+
+= $message ?>
diff --git a/src/Template/Element/Flash/success.ctp b/src/Template/Element/Flash/success.ctp
new file mode 100644
index 0000000..becd5a1
--- /dev/null
+++ b/src/Template/Element/Flash/success.ctp
@@ -0,0 +1,6 @@
+
+= $message ?>
diff --git a/src/Template/Element/header.ctp b/src/Template/Element/header.ctp
new file mode 100644
index 0000000..861478e
--- /dev/null
+++ b/src/Template/Element/header.ctp
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/src/Template/Email/html/default.ctp b/src/Template/Email/html/default.ctp
new file mode 100644
index 0000000..386674a
--- /dev/null
+++ b/src/Template/Email/html/default.ctp
@@ -0,0 +1,22 @@
+
+ ' . $line . "
\n";
+endforeach;
+?>
diff --git a/src/Template/Email/text/default.ctp b/src/Template/Email/text/default.ctp
new file mode 100644
index 0000000..704b46f
--- /dev/null
+++ b/src/Template/Email/text/default.ctp
@@ -0,0 +1,16 @@
+
+= $content ?>
diff --git a/src/Template/Error/error400.ctp b/src/Template/Error/error400.ctp
new file mode 100644
index 0000000..2aebac6
--- /dev/null
+++ b/src/Template/Error/error400.ctp
@@ -0,0 +1,38 @@
+layout = 'error';
+
+if (Configure::read('debug')):
+ $this->layout = 'dev_error';
+
+ $this->assign('title', $message);
+ $this->assign('templateName', 'error400.ctp');
+
+ $this->start('file');
+?>
+queryString)) : ?>
+
+ SQL Query:
+ = h($error->queryString) ?>
+
+
+params)) : ?>
+ SQL Query Params:
+ params) ?>
+
+= $this->element('auto_table_warning') ?>
+end();
+endif;
+?>
+= h($message) ?>
+
+ = __d('cake', 'Error') ?>:
+ = __d('cake', 'The requested address {0} was not found on this server.', "'{$url}' ") ?>
+
diff --git a/src/Template/Error/error500.ctp b/src/Template/Error/error500.ctp
new file mode 100644
index 0000000..d8014f8
--- /dev/null
+++ b/src/Template/Error/error500.ctp
@@ -0,0 +1,43 @@
+layout = 'error';
+
+if (Configure::read('debug')):
+ $this->layout = 'dev_error';
+
+ $this->assign('title', $message);
+ $this->assign('templateName', 'error500.ctp');
+
+ $this->start('file');
+?>
+queryString)) : ?>
+
+ SQL Query:
+ = h($error->queryString) ?>
+
+
+params)) : ?>
+ SQL Query Params:
+ params) ?>
+
+
+ Error in:
+ = sprintf('%s, line %s', str_replace(ROOT, 'ROOT', $error->getFile()), $error->getLine()) ?>
+
+element('auto_table_warning');
+
+ if (extension_loaded('xdebug')):
+ xdebug_print_function_stack();
+ endif;
+
+ $this->end();
+endif;
+?>
+= __d('cake', 'An Internal Error Has Occurred') ?>
+
+ = __d('cake', 'Error') ?>:
+ = h($message) ?>
+
diff --git a/src/Template/Layout/Email/html/default.ctp b/src/Template/Layout/Email/html/default.ctp
new file mode 100644
index 0000000..2b43970
--- /dev/null
+++ b/src/Template/Layout/Email/html/default.ctp
@@ -0,0 +1,24 @@
+
+
+
+
+ = $this->fetch('title') ?>
+
+
+ = $this->fetch('content') ?>
+
+
diff --git a/src/Template/Layout/Email/text/default.ctp b/src/Template/Layout/Email/text/default.ctp
new file mode 100644
index 0000000..871dcfb
--- /dev/null
+++ b/src/Template/Layout/Email/text/default.ctp
@@ -0,0 +1,16 @@
+
+= $this->fetch('content') ?>
diff --git a/src/Template/Layout/ajax.ctp b/src/Template/Layout/ajax.ctp
new file mode 100644
index 0000000..871dcfb
--- /dev/null
+++ b/src/Template/Layout/ajax.ctp
@@ -0,0 +1,16 @@
+
+= $this->fetch('content') ?>
diff --git a/src/Template/Layout/default.ctp b/src/Template/Layout/default.ctp
new file mode 100644
index 0000000..0352a67
--- /dev/null
+++ b/src/Template/Layout/default.ctp
@@ -0,0 +1,54 @@
+
+
+
+ = $this->Html->charset(); ?>
+
+
+ LBRY Block Explorer • = $this->fetch('title') ?>
+
+ Html->meta('icon') */?>
+
+ Html->script('jquery.js') ?>
+ Html->script('moment.js') ?>
+
+ Html->css('main.css') ?>
+
+
+
+
+
+
+
+
+
+
+ fetch('meta') ?>
+ fetch('css') ?>
+ fetch('script') ?>
+
+
+ fetch('content') ?>
+
+
+
+
diff --git a/src/Template/Layout/error.ctp b/src/Template/Layout/error.ctp
new file mode 100644
index 0000000..5304ec4
--- /dev/null
+++ b/src/Template/Layout/error.ctp
@@ -0,0 +1,47 @@
+
+
+
+
+ = $this->Html->charset() ?>
+
+ = $this->fetch('title') ?>
+
+ = $this->Html->meta('icon') ?>
+
+ = $this->Html->css('base.css') ?>
+ = $this->Html->css('cake.css') ?>
+
+ = $this->fetch('meta') ?>
+ = $this->fetch('css') ?>
+ = $this->fetch('script') ?>
+
+
+
+
+
+ = $this->Flash->render() ?>
+
+ = $this->fetch('content') ?>
+
+
+
+
+
diff --git a/src/Template/Layout/rss/default.ctp b/src/Template/Layout/rss/default.ctp
new file mode 100644
index 0000000..5c15a19
--- /dev/null
+++ b/src/Template/Layout/rss/default.ctp
@@ -0,0 +1,14 @@
+fetch('title');
+endif;
+
+echo $this->Rss->document(
+ $this->Rss->channel(
+ [], $channel, $this->fetch('content')
+ )
+);
+?>
diff --git a/src/Template/Main/address.ctp b/src/Template/Main/address.ctp
new file mode 100644
index 0000000..5b4bfc8
--- /dev/null
+++ b/src/Template/Main/address.ctp
@@ -0,0 +1,186 @@
+assign('title', 'Address ' . $address->Address) ?>
+
+start('script') ?>
+
+end() ?>
+
+element('header') ?>
+
+
+
LBRY Address
+
Address ?>
+
+
+
Tag adddress
+
+
This address has a pending tag request. If you made this request, please send exactly
VerificationAmount ?> LBC from
Address ?> to
bLockNgmfvnnnZw7bM6SPz6hk5BVzhevEp . Incomplete requests will be automatically deleted 7 days from the request creation date.
+
+
Label your public LBRY address with a name and an optional link. In order to tag this address, please send exactly
LBC
+ from
Address ?> to
bLockNgmfvnnnZw7bM6SPz6hk5BVzhevEp
+ and then specify the desired tag (maximum 30 characters) and link in the fields below. The transaction will be verified after at least 1 confirmation. The
LBC fee is a measure to prevent spam and low-effort submissions. Verification is an automatic process, but any tags or URLs that may be considered illegal when brought to attention will be removed.
+
+
+
+
+
+
+
+
+
+
+
+
+
Received (LBC)
+
Amount->format($totalReceived) ?>
+
+
+
+
Sent (LBC)
+
Amount->format($totalSent) ?>
+
+
+
+
Balance (LBC)
+
Amount->format($balanceAmount) ?>
+
+
+
+
+
+
+
+
+
+
Recent Transactions
+
+
+
+ Height
+ Transaction Hash
+ Timestamp
+ Confirmations
+ Inputs
+ Outputs
+ Amount
+
+
+
+
+
+
+ There are no recent transactions to display for this wallet.
+
+
+
+
+
+ Height === null): ?>Unconfirmed Height ?>
+
+ TxTime)->format('j M Y H:i:s') . ' UTC'; ?>
+ Confirmations, 0, '', ',') ?>
+ InputCount ?>
+ OutputCount ?>
+
+ DebitAmount > 0) ? '-' : '+'); ?>DebitAmount > 0) ? $tx->DebitAmount : $tx->CreditAmount), 8, '.', '') ?> LBC
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Template/Main/blocks.ctp b/src/Template/Main/blocks.ctp
new file mode 100644
index 0000000..55283c5
--- /dev/null
+++ b/src/Template/Main/blocks.ctp
@@ -0,0 +1,127 @@
+element('header') ?>
+
+
+ start('script'); ?>
+
+ end(); ?>
+
+ assign('title', 'Block Height ' . $block->Height) ?>
+
+
+
LBRY Block Height ?>
+ Hash ?>
+
+
+
+
+
+
+
Overview
+
+
Block Size (bytes)
+
Block Time
+
+
BlockSize, 0, '', ',') ?>
+
BlockTime)->format('j M Y H:i:s') . ' UTC' ?>
+
+
+
+
Bits
+
Confirmations
+
+
Bits ?>
+
Confirmations, 0, '', ',') ?>
+
+
+
+
Difficulty
+
Nonce
+
+
Amount->format($block->Difficulty) ?>
+
Nonce ?>
+
+
+
+
Chainwork
Chainwork ?>
+
+
+
+
MerkleRoot
MerkleRoot ?>
+
+
+
+
NameClaimRoot
NameClaimRoot ?>
+
+
+
+
Target
Target ?>
+
+
+
+
Version
Version ?>
+
+
+
+
Transaction
+
+
+
+
+
+ Hash
+ Inputs
+ Outputs
+ Value
+
+
+
+
+
+ There are no transactions to display at this time.
+
+
+
+
+
+
+ InputCount ?>
+ OutputCount ?>
+ Amount->formatCurrency($tx->Value) ?> LBC
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Template/Main/index.ctp b/src/Template/Main/index.ctp
new file mode 100644
index 0000000..a9aa417
--- /dev/null
+++ b/src/Template/Main/index.ctp
@@ -0,0 +1,147 @@
+assign('title', 'Home') ?>
+
+start('script'); ?>
+
+end(); ?>
+
+
+
+
+
LBRY Block Explorer
+
+
+
+
+
+
Block Height
+
Height ?>
+
+
+
+
Difficulty
+
Difficulty, 2, '.', '') ?>
+
+
+
+
+
+
+
+
+
+
+
Recent Blocks
+
+
+
+ Height
+ Age
+ Block Size
+ Transactions
+ Difficulty
+ Timestamp
+
+
+
+
+
+
+ Height ?>
+ BlockTime)->diffForHumans(); ?>
+ BlockSize / 1024, 2) . 'KB' ?>
+ TransactionCount ?>
+ Difficulty, 2, '.', '') ?>
+ BlockTime)->format('j M Y H:i:s') . ' UTC' ?>
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Template/Main/realtime.ctp b/src/Template/Main/realtime.ctp
new file mode 100644
index 0000000..3b29f04
--- /dev/null
+++ b/src/Template/Main/realtime.ctp
@@ -0,0 +1,170 @@
+assign('title', 'Realtime Explorer') ?>
+
+start('script'); ?>
+
+end(); ?>
+
+element('header') ?>
+
+
+
Realtime Explorer
+
+
+
+
+
Recent Blocks
+
+
+
+ Height
+ Age
+ # TXs
+
+
+
+
+
+
+ Height ?>
+ BlockTime)->diffForHumans(); ?>
+ TransactionCount ?>
+
+
+
+
+
+
+
+
Recent Transactions
+
+
+
+ Hash
+ Time
+ Inputs
+ Outputs
+ Amount
+
+
+
+
+
+
+
+ TxTime)->diffForHumans(); ?>
+ InputCount ?>
+ OutputCount ?>
+ Value, 8, '.', '') ?> LBC
+
+
+
+
+
+
+
+
+
diff --git a/src/Template/Main/tx.ctp b/src/Template/Main/tx.ctp
new file mode 100644
index 0000000..6277392
--- /dev/null
+++ b/src/Template/Main/tx.ctp
@@ -0,0 +1,142 @@
+assign('title', 'Transaction ' . $tx->Hash) ?>
+
+start('script'); ?>
+
+end(); ?>
+
+element('header') ?>
+
+
+
LBRY Transaction
+ Hash ?>
+
+
+
+
+
Amount (LBC)
+
Amount->format($tx->Value) ?>
+
+
+
+
Block Height
+ BlockHash) || strlen(trim($tx->BlockHash)) === 0): ?>
+
Unconf.
+
+
+
+
+
+
+
+
+
Size (bytes)
+
TransactionSize, 0, '', ',') ?>
+
+
+
+
Inputs
+
InputCount ?>
+
+
+
+
Outputs
+
OutputCount ?>
+
+
+
+
+
+
+
Details
+
+
+
+
+
+
+
+
+
OutputCount ?> outputOutputCount === 1 ? '' : 's'; ?>
+
+ 0): ?>
+ Fee Amount->format($fee) ?> LBC
+
+
+
+
+
+
+ IsSupportClaim): ?>
SUPPORT
+ IsUpdateClaim): ?>
UPDATE
+ IsClaim): ?>
CLAIM
+
+
+
+
Incomplete data
+ Address])):
+ $setAddressIds[$addr->Address] = 1; ?>
+
+
+
Amount->format($out['Value']) ?> LBC to
+
Address ?>
+
+ IsSpent): ?>(
spent )(unspent)
+
+ Tag) && strlen(trim($addr->Tag)) > 0): ?>
+
+ TagUrl)) > 0): ?>
Tag ?> Tag; endif; ?>
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Template/Pages/home.ctp b/src/Template/Pages/home.ctp
new file mode 100644
index 0000000..cae07dd
--- /dev/null
+++ b/src/Template/Pages/home.ctp
@@ -0,0 +1,276 @@
+layout = false;
+
+if (!Configure::read('debug')):
+ throw new NotFoundException('Please replace src/Template/Pages/home.ctp with your own version.');
+endif;
+
+$cakeDescription = 'CakePHP: the rapid development PHP framework';
+?>
+
+
+
+ = $this->Html->charset() ?>
+
+
+ = $cakeDescription ?>
+
+
+ = $this->Html->meta('icon') ?>
+ = $this->Html->css('base.css') ?>
+ = $this->Html->css('cake.css') ?>
+ = $this->Html->css('home.css') ?>
+
+
+
+
+
+
+
+
+
+
Please be aware that this page will not be shown if you turn off debug mode unless you replace src/Template/Pages/home.ctp with your own version.
+
+
+
+
+
+
+
+
+
Environment
+
+ =')): ?>
+ Your version of PHP is 5.6.0 or higher (detected = PHP_VERSION ?>).
+
+ Your version of PHP is too low. You need PHP 5.6.0 or higher to use CakePHP (detected = PHP_VERSION ?>).
+
+
+
+ Your version of PHP has the mbstring extension loaded.
+
+ Your version of PHP does NOT have the mbstring extension loaded. ;
+
+
+
+ Your version of PHP has the openssl extension loaded.
+
+ Your version of PHP has the mcrypt extension loaded.
+
+ Your version of PHP does NOT have the openssl or mcrypt extension loaded.
+
+
+
+ Your version of PHP has the intl extension loaded.
+
+ Your version of PHP does NOT have the intl extension loaded.
+
+
+
+
+
Filesystem
+
+
+ Your tmp directory is writable.
+
+ Your tmp directory is NOT writable.
+
+
+
+ Your logs directory is writable.
+
+ Your logs directory is NOT writable.
+
+
+
+
+ The = $settings['className'] ?>Engine is being used for core caching. To change the config edit config/app.php
+
+ Your cache is NOT working. Please check the settings in config/app.php
+
+
+
+
+
+
+
+
+
Database
+ connect();
+ } catch (Exception $connectionError) {
+ $connected = false;
+ $errorMsg = $connectionError->getMessage();
+ if (method_exists($connectionError, 'getAttributes')):
+ $attributes = $connectionError->getAttributes();
+ if (isset($errorMsg['message'])):
+ $errorMsg .= '
' . $attributes['message'];
+ endif;
+ endif;
+ }
+ ?>
+
+
+ CakePHP is able to connect to the database.
+
+ CakePHP is NOT able to connect to the database. = $errorMsg ?>
+
+
+
+
+
DebugKit
+
+
+ DebugKit is loaded.
+
+ DebugKit is NOT loaded. You need to either install pdo_sqlite, or define the "debug_kit" connection name.
+
+
+
+
+
+
+
+
+
Editing this Page
+
+ To change the content of this page, edit: src/Template/Pages/home.ctp.
+ You can also add some CSS styles for your pages at: webroot/css/.
+
+
+
+
+
+
+
+
More about Cake
+
+ CakePHP is a rapid development framework for PHP which uses commonly known design patterns like Front Controller and MVC.
+ Our primary goal is to provide a structured framework that enables PHP users at all levels to rapidly develop robust web applications, without any loss to flexibility.
+
+
+
+
+
+
+
+
P
+
Help and Bug Reports
+
+
+
+
r
+
Docs and Downloads
+
+
+
+
s
+
Training and Certification
+
+
+
+
+
+
diff --git a/src/View/AjaxView.php b/src/View/AjaxView.php
new file mode 100644
index 0000000..594e2d8
--- /dev/null
+++ b/src/View/AjaxView.php
@@ -0,0 +1,49 @@
+response->type('ajax');
+ }
+}
diff --git a/src/View/AppView.php b/src/View/AppView.php
new file mode 100644
index 0000000..630ed93
--- /dev/null
+++ b/src/View/AppView.php
@@ -0,0 +1,42 @@
+loadHelper('Html');`
+ *
+ * @return void
+ */
+ public function initialize()
+ {
+ parent::initialize();
+ $this->loadHelper('Amount');
+ }
+}
diff --git a/src/View/Helper/AmountHelper.php b/src/View/Helper/AmountHelper.php
new file mode 100644
index 0000000..ee6bcbb
--- /dev/null
+++ b/src/View/Helper/AmountHelper.php
@@ -0,0 +1,44 @@
+ 0) {
+ $value .= '.' . rtrim($right, '0');
+ }
+ }
+
+ return $value;
+ }
+
+ public function formatCurrency($value) {
+ $dotIdx = strpos($value, '.');
+ if ($dotIdx !== false) {
+ $left = substr($value, 0, $dotIdx);
+ $right = substr($value, $dotIdx + 1);
+
+ $value = number_format($left, 0, '', ',');
+ if ((int) $right > 0) {
+ if (strlen($right) === 1) {
+ $value .= '.' . $right . '0';
+ } else {
+ $value .= '.' . substr($right, 0, 2);
+ }
+ }
+ }
+
+ return $value;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/tests/TestCase/ApplicationTest.php b/tests/TestCase/ApplicationTest.php
new file mode 100644
index 0000000..d397fa1
--- /dev/null
+++ b/tests/TestCase/ApplicationTest.php
@@ -0,0 +1,46 @@
+middleware($middleware);
+
+ $this->assertInstanceOf(ErrorHandlerMiddleware::class, $middleware->get(0));
+ $this->assertInstanceOf(AssetMiddleware::class, $middleware->get(1));
+ $this->assertInstanceOf(RoutingMiddleware::class, $middleware->get(2));
+ }
+}
diff --git a/tests/TestCase/Controller/PagesControllerTest.php b/tests/TestCase/Controller/PagesControllerTest.php
new file mode 100644
index 0000000..1b478a3
--- /dev/null
+++ b/tests/TestCase/Controller/PagesControllerTest.php
@@ -0,0 +1,97 @@
+get('/');
+ $this->assertResponseOk();
+ $this->get('/');
+ $this->assertResponseOk();
+ }
+
+ /**
+ * testDisplay method
+ *
+ * @return void
+ */
+ public function testDisplay()
+ {
+ $this->get('/pages/home');
+ $this->assertResponseOk();
+ $this->assertResponseContains('CakePHP');
+ $this->assertResponseContains('');
+ }
+
+ /**
+ * Test that missing template renders 404 page in production
+ *
+ * @return void
+ */
+ public function testMissingTemplate()
+ {
+ Configure::write('debug', false);
+ $this->get('/pages/not_existing');
+
+ $this->assertResponseError();
+ $this->assertResponseContains('Error');
+ }
+
+ /**
+ * Test that missing template in debug mode renders missing_template error page
+ *
+ * @return void
+ */
+ public function testMissingTemplateInDebug()
+ {
+ Configure::write('debug', true);
+ $this->get('/pages/not_existing');
+
+ $this->assertResponseFailure();
+ $this->assertResponseContains('Missing Template');
+ $this->assertResponseContains('Stacktrace');
+ $this->assertResponseContains('not_existing.ctp');
+ }
+
+ /**
+ * Test directory traversal protection
+ *
+ * @return void
+ */
+ public function testDirectoryTraversalProtection()
+ {
+ $this->get('/pages/../Layout/ajax');
+ $this->assertResponseCode(403);
+ $this->assertResponseContains('Forbidden');
+ }
+}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
new file mode 100644
index 0000000..0ca191e
--- /dev/null
+++ b/tests/bootstrap.php
@@ -0,0 +1,12 @@
+
+ RewriteEngine On
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteRule ^ index.php [L]
+
diff --git a/webroot/css/main.css b/webroot/css/main.css
new file mode 100644
index 0000000..64a2fc4
--- /dev/null
+++ b/webroot/css/main.css
@@ -0,0 +1,166 @@
+* { box-sizing: border-box; font-family: 'jaf-facitweb', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale }
+*:focus { outline: none }
+.home-container { display: table; width: 100%; height: 100% }
+.home-container-cell { display: table-cell; vertical-align: middle; min-height: 400px }
+.main { display: block; width: 1200px; margin: 0 auto }
+a:link, a:visited { color: #1e88e5; text-decoration: none }
+a:hover { text-decoration: underline; color: #1976d2 }
+
+.header { width: 1200px; margin: 0 auto 24px auto; padding: 12px 0 64px 0; border-bottom: 1px solid #ddd }
+.header a:link, .header a:visited { color: #333; text-decoration: none }
+.header .title { font-family: 'adelle-sans', sans-serif; font-weight: bold; font-size: 140%; float: left; position: relative; top: 4px }
+.header .search { float: right }
+.header .search .input-group { border: 2px solid #1e88e5; position: relative; width: 525px; height: 36px; border-radius: 8px }
+.header .search .input-group input { border: none; border-radius: 8px; padding: 8px 133px 8px 8px; width: 100%; height: 32px; width: 521px }
+.header .search .input-group .btn-inline-search { border: none; cursor: pointer; position: absolute; right: -4px; top: 0; height: 100%; background: #1e88e5; color: #fff; width: 125px;
+border-radius: 0 6px 8px 0 }
+.header .search .input-group .btn-inline-search:hover { background: #1976d2 }
+
+.home-container-cell > .main > .title { font-family: 'adelle-sans', sans-serif; font-weight: bold; font-size: 280%; margin: 24px auto 24px auto; width: 600px; text-align: center; color: #333; cursor: default }
+.home-container-cell .search-input { display: block; margin: 0 auto; padding: 8px; text-align: center; border: 3px solid #ddd; border-radius: 16px; width: 600px; font-size: 115%; font-weight: 300 }
+.home-container-cell .ctls { width: 600px; text-align: center; margin: 24px auto; position: relative }
+.home-container-cell .ctls .btn-search { font-size: 115%; display: inline-block; padding: 12px 48px; background: #1e88e5; color: #fff; border-radius: 8px; border: none; font-weight: 300; cursor: pointer }
+.home-container-cell .ctls .btn-search:hover { background: #1976d2 }
+.home-container-cell .ctls a { font-size: 115%; display: inline-block; font-weight: 300; position: absolute; right: 0; padding-top: 12px }
+.home-container-cell .ctls a:hover { text-decoration: none; color: #1976d2 }
+
+.home-container-cell .recent-blocks { width: 1000px; margin: 48px auto 0 auto; box-shadow: 0 6px 12px rgba(0,0,0,.175); border: 1px solid rgba(0,0,0,.15); padding: 24px 36px 36px 36px; cursor: default }
+.home-container-cell .recent-blocks h3 { font-weight: normal; margin: 0 0 12px 0; font-weight: 300 }
+
+.table { width: 100%; cursor: default; border-collapse: collapse; font-size: 90% }
+.table thead tr th { border-top: 1px solid #ddd; border-bottom: 1px solid #ddd; padding: 12px 4px }
+.table tbody tr td { padding: 8px; border-bottom: 1px solid #eee; font-weight: 300; white-space: nowrap }
+.table tbody tr td.nodata { text-align: center; font-style: italic; font-weight: 300 }
+.table .last-cell { padding-left: 48px }
+
+.left { text-align: left }
+.center { text-align: center }
+.right { text-align: right }
+
+.w80 { width: 80px }
+.w100 { width: 100px }
+.w125 { width: 125px }
+.w150 { width: 150px }
+.w225 { width: 225px }
+.w200, .w200 > div { width: 200px }
+.w250, .w250 > div { width: 250px }
+.w275, .w275 > div { width: 275px }
+.w300, .w300 > div { width: 300px }
+.w200 > div, .w250 > div, .w275 > div, .w300 > div { overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
+
+footer { padding-top: 64px; font-size: 80%; cursor: default }
+footer .content { width: 1200px; margin: 0 auto; border-top: 1px solid #ddd; padding: 24px 12px 48px 12px; line-height: 25px; font-weight: 300; position: relative }
+footer .content .page-time { position: absolute; right: 12px; bottom: 0px; padding-bottom: 52px; font-size: 85%; color: #ccc }
+
+.block-head { width: 1200px; margin: 0 auto 24px auto; cursor: default }
+.block-head h3, h4 { font-weight: 300; margin: 0 }
+.block-head h3 { font-size: 200%; margin-bottom: 3px }
+.block-head h4 { font-size: 125% }
+
+.block-info { width: 1200px; margin: 0 auto }
+.block-info h3 { font-weight: normal; margin: 0 0 12px 0; font-weight: 300 }
+
+.block-nav { width: 1200px; margin: 0 auto 24px auto }
+.block-nav .btn { display: block; padding: 8px; width: 150px; text-align: center; border-radius: 3px; border: 1px solid #ddd; font-size: 80% }
+.block-nav .btn:link, .block-nav .btn:visited { text-decoration: none; color: #333 }
+.block-nav .btn:hover { background: #f1f1f1 }
+
+.block-nav .btn-prev { float: left }
+.block-nav .btn-next { float: right }
+
+.block-summary { width: 460px; margin: 0; box-shadow: 0 2px 6px rgba(0,0,0,.175); border: 1px solid rgba(0,0,0,.15); padding: 36px; cursor: default; float: left }
+.block-summary .label { font-size: 80%; color: #1e88e5 }
+.block-summary .value { font-weight: 300; word-break: break-word; word-wrap: break-word; font-size: 95% }
+.block-summary .half-width { width: 50%; float: left }
+.block-summary .spacer { height: 16px }
+
+.block-transactions { width: 690px; margin: 0 0 0 50px; box-shadow: 0 2px 6px rgba(0,0,0,.175); border: 1px solid rgba(0,0,0,.15); padding: 36px; cursor: default; float: left; overflow: auto; overflow-x: hidden }
+
+.tx-head { width: 1200px; margin: 0 auto 24px auto; cursor: default }
+.tx-head h3, h4 { font-weight: 300; margin: 0 }
+.tx-head h3 { font-size: 200%; margin-bottom: 3px }
+.tx-head h4 { font-size: 125% }
+
+.realtime-head { width: 1200px; margin: 0 auto 36px auto; cursor: default }
+.realtime-head h3 { font-weight: 300; margin: 0; font-size: 200% }
+.realtime-main { width: 1200px; margin: 0 auto 0 auto }
+.realtime-main h3 { font-weight: 300; margin: 0 0 12px 0 }
+.realtime-main .realtime-blocks { width: 350px; box-shadow: 0 2px 6px rgba(0,0,0,.175); border: 1px solid rgba(0,0,0,.15); padding: 24px; cursor: default; float: left }
+.realtime-main .realtime-tx { width: 800px; margin-left: 50px; box-shadow: 0 2px 6px rgba(0,0,0,.175); border: 1px solid rgba(0,0,0,.15); padding: 24px; cursor: default; float: left }
+
+.stats { width: 1000px; margin: 0 auto 48px auto; box-shadow: 0 2px 6px rgba(0,0,0,.175); border: 1px solid rgba(0,0,0,.15); padding: 24px; cursor: default }
+.stats .box { padding: 24px 0; border-right: 1px solid #ccc; float: left; text-align: center; width: 25% }
+.stats .box .title { color: #1e88e5; font-size: 90% }
+.stats .box .value { font-size: 180%; font-weight: 300; margin-top: 8px }
+.stats .box.last { border-color: transparent }
+
+.tx-summary { width: 1200px; margin: 0 auto; box-shadow: 0 2px 6px rgba(0,0,0,.175); border: 1px solid rgba(0,0,0,.15); padding: 16px 0; cursor: default }
+.tx-summary .box { padding: 24px 0; border-right: 1px solid #ccc; float: left; text-align: center; width: 20% }
+.tx-summary .box.p15 { width: 15% }
+.tx-summary .box.p25 { width: 25% }
+.tx-summary .box .title { color: #1e88e5; font-size: 90% }
+.tx-summary .box .value { font-size: 180%; font-weight: 300; margin-top: 8px }
+.tx-summary .box.last { border-color: transparent }
+
+.tx-details { width: 1200px; margin: 48px auto 0 auto; cursor: default }
+.tx-details h3 { font-weight: 300; margin: 0 0 8px 0 }
+.tx-details-layout { display: table; width: 100%; box-shadow: 0 2px 6px rgba(0,0,0,.175); border: 1px solid rgba(0,0,0,.15); padding: 24px; background: #f9f9f9 }
+
+.tx-details-layout .divider { width: 20%; display: table-cell; text-align: center; vertical-align: top; padding-top: 40px }
+.tx-details-layout .divider img { width: 72px }
+.tx-details-layout .inputs, .tx-details-layout .outputs { width: 40%; display: table-cell; vertical-align: top }
+.tx-details-layout .inputs .subtitle, .tx-details-layout .outputs .subtitle { margin-bottom: 16px; position: relative; line-height: 24px }
+.tx-details-layout .outputs .subtitle .fee { position: absolute; display: block; right: 0; bottom: 0 }
+.tx-details-layout .outputs .subtitle .fee .value { font-weight: 300; display: inline-block; margin-left: 16px }
+.tx-details-layout .inputs .input, .tx-details-layout .outputs .output { border: 1px solid #ddd; padding: 16px; border-radius: 18px; font-size: 95%; font-weight: 300; margin-bottom: 16px; background: #fff; position: relative }
+.tx-details-layout .tag { font-weight: normal; margin-top: 1px; font-size: 80%; color: #333 }
+.tx-details-layout .tag a:link, .tx-details-layout .tag a:visited { color: #333 }
+.tx-details-layout .tag a:hover { text-decoration: none }
+
+.tx-details-layout .outputs .output .labels { position: absolute; right: 16px; top: 8px }
+.tx-details-layout .outputs .output .labels > div { padding: 4px 12px; font-size: 70%; display: inline-block; margin-left: 4px }
+.tx-details-layout .outputs .output .labels .support { background: #ffeb3b }
+.tx-details-layout .outputs .output .labels .update { background: #ea80fc }
+.tx-details-layout .outputs .output .labels .claim { background: #76ff03 }
+.tx-details-layout .inputs .input .value, .tx-details-layout .outputs .output .value { font-weight: normal }
+.tx-details-layout .inputs .input.is-source, .tx-details-layout .outputs .output.is-source { border-right-width: 18px; border-right-color: #1e88e5; background: #e3f2fd }
+.tx-details-layout .inputs .input.highlighted, .tx-details-layout .outputs .output.highlighted { background: #f1f8e9 }
+.tx-details-layout .outputs .output.is-source .labels { right: 8px }
+
+.address-head { width: 1200px; margin: 0 auto 48px auto; cursor: default }
+.address-head h3, h4 { font-weight: 300; margin: 0 }
+.address-head h3 { font-size: 200%; margin-bottom: 3px }
+.address-head h4 { font-size: 125%; display: inline-block }
+.address-head .tag { display: inline-block; font-size: 80%; color: #1e88e5; margin-left: 8px }
+.address-head .tag a:link, .address-head .tag a:visited { color: #1e88e5 }
+.address-head .tag a:hover { text-decoration: none }
+
+.address-head .tag-address-container { margin: 24px 0; box-shadow: 0 2px 6px rgba(0,0,0,.175); border: 1px solid rgba(0,0,0,.15); padding: 24px; cursor: default; font-size: 80%; display: none }
+.address-head .tag-address-container h4 { margin-bottom: 6px }
+.address-head .tag-address-container .desc { line-height: 22px; font-weight: 300 }
+.address-head .tag-address-container .form-group { margin-top: 12px }
+.address-head .tag-address-container .form-group .error-message { margin-bottom: 6px; color: #ff0000; font-style: italic; font-size: 105%; height: 24px; line-height: 24px }
+.address-head .tag-address-container .form-group .col { float: left; width: 300px; margin-right: 12px }
+.address-head .tag-address-container .form-group .col input { display: block; border: 2px solid #ccc; padding: 6px; margin-bottom: 12px; border-radius: 4px; width: 300px; line-height: 24px }
+.address-head .tag-address-container .form-group .col input:last-child { margin-bottom: 0 }
+.address-head .tag-address-container .form-group .col textarea { display: block; border: 2px solid #ccc; width: 300px; padding: 6px; line-height: 24px; height: 92px; border-radius: 4px; resize: none; position: relative; top: -1px; font-size: 100% }
+.address-head .tag-address-container .btn { display: inline-block; padding: 6px 36px; height: 39px; line-height: 24px; color: #fff; border-radius: 4px; font-weight: 300; cursor: pointer; border: none }
+.address-head .tag-address-container .btn-tag { background: #1e88e5 }
+.address-head .tag-address-container .btn-close { background: #ff0000; margin-left: 12px }
+
+.address-subhead { width: 1200px; margin: 0 auto }
+.address-qr { width: 170px; height: 170px; float: left }
+.address-qr img { width: 170px; height: 170px; border: 1px solid rgba(0,0,0,.15); padding: 3px }
+.address-summary { width: 982px; float: left; margin-left: 48px; box-shadow: 0 2px 6px rgba(0,0,0,.175); border: 1px solid rgba(0,0,0,.15); padding: 25px 0 26px 0; cursor: default }
+.address-summary .box { padding: 24px 0; border-right: 1px solid #ccc; float: left; text-align: center; width: 33% }
+.address-summary .box .title { color: #1e88e5; font-size: 90% }
+.address-summary .box .value { font-size: 180%; font-weight: 300; margin-top: 8px }
+.address-summary .box.last { border-color: transparent }
+
+.recent-transactions h3 { font-weight: 300; margin: 0; margin-bottom: 12px }
+.recent-transactions { width: 1200px; margin: 48px auto 0 auto; box-shadow: 0 2px 6px rgba(0,0,0,.175); border: 1px solid rgba(0,0,0,.15); padding: 36px; cursor: default }
+
+.tx-table .credit { color: #00e676 }
+.tx-table .debit { color: #ff0000 }
+
+.clear { clear: both }
\ No newline at end of file
diff --git a/webroot/img/right-arrow.png b/webroot/img/right-arrow.png
new file mode 100644
index 0000000..e13098a
Binary files /dev/null and b/webroot/img/right-arrow.png differ
diff --git a/webroot/index.php b/webroot/index.php
new file mode 100644
index 0000000..6791f47
--- /dev/null
+++ b/webroot/index.php
@@ -0,0 +1,37 @@
+emit($server->run());
diff --git a/webroot/js/jquery.js b/webroot/js/jquery.js
new file mode 100644
index 0000000..644d35e
--- /dev/null
+++ b/webroot/js/jquery.js
@@ -0,0 +1,4 @@
+/*! jQuery v3.2.1 | (c) JS Foundation and other contributors | jquery.org/license */
+!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.2.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML=" ",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML=" ";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML=" ","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML=" ",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext;function B(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()}var C=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,D=/^.[^:#\[\.,]*$/;function E(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):D.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(E(this,a||[],!1))},not:function(a){return this.pushStack(E(this,a||[],!0))},is:function(a){return!!E(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var F,G=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,H=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||F,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:G.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),C.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};H.prototype=r.fn,F=r(d);var I=/^(?:parents|prev(?:Until|All))/,J={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function K(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return K(a,"nextSibling")},prev:function(a){return K(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return B(a,"iframe")?a.contentDocument:(B(a,"template")&&(a=a.content||a),r.merge([],a.childNodes))}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(J[a]||r.uniqueSort(e),I.test(a)&&e.reverse()),this.pushStack(e)}});var L=/[^\x20\t\r\n\f]+/g;function M(a){var b={};return r.each(a.match(L)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?M(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=e||a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function N(a){return a}function O(a){throw a}function P(a,b,c,d){var e;try{a&&r.isFunction(e=a.promise)?e.call(a).done(b).fail(c):a&&r.isFunction(e=a.then)?e.call(a,b,c):b.apply(void 0,[a].slice(d))}catch(a){c.apply(void 0,[a])}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==O&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:N,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:N)),c[2][3].add(g(0,a,r.isFunction(d)?d:O))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(P(a,g.done(h(c)).resolve,g.reject,!b),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)P(e[c],h(c),g.reject);return g.promise()}});var Q=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&Q.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var R=r.Deferred();r.fn.ready=function(a){return R.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||R.resolveWith(d,[r]))}}),r.ready.then=R.then;function S(){d.removeEventListener("DOMContentLoaded",S),
+a.removeEventListener("load",S),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",S),a.addEventListener("load",S));var T=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)T(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){X.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=W.get(a,b),c&&(!d||Array.isArray(c)?d=W.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return W.get(a,c)||W.access(a,c,{empty:r.Callbacks("once memory").add(function(){W.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,la=/^$|\/(?:java|ecma)script/i,ma={option:[1,""," "],thead:[1,""],col:[2,""],tr:[2,""],td:[3,""],_default:[0,"",""]};ma.optgroup=ma.option,ma.tbody=ma.tfoot=ma.colgroup=ma.caption=ma.thead,ma.th=ma.td;function na(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&B(a,b)?r.merge([a],c):c}function oa(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=na(l.appendChild(f),"script"),j&&oa(g),c){k=0;while(f=g[k++])la.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var ra=d.documentElement,sa=/^key/,ta=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ua=/^([^.]*)(?:\.(.+)|)/;function va(){return!0}function wa(){return!1}function xa(){try{return d.activeElement}catch(a){}}function ya(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ya(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=wa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(ra,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(L)||[""],j=b.length;while(j--)h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.hasData(a)&&W.get(a);if(q&&(i=q.events)){b=(b||"").match(L)||[""],j=b.length;while(j--)if(h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&W.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(W.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,Aa=/