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 @@ + +
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 @@ + +
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 @@ + +
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 @@ + + 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: + queryString) ?> +

+ +params)) : ?> + SQL Query Params: + params) ?> + +element('auto_table_warning') ?> +end(); +endif; +?> +

+

+ : + '{$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: + queryString) ?> +

+ +params)) : ?> + SQL Query Params: + params) ?> + + + Error in: + getFile()), $error->getLine()) ?> + +element('auto_table_warning'); + + if (extension_loaded('xdebug')): + xdebug_print_function_stack(); + endif; + + $this->end(); +endif; +?> +

+

+ : + +

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') ?> + + + 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 @@ + +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 @@ + +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 @@ + + + + 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 @@ + + + + + Html->charset() ?> + + <?= $this->fetch('title') ?> + + Html->meta('icon') ?> + + Html->css('base.css') ?> + Html->css('cake.css') ?> + + fetch('meta') ?> + fetch('css') ?> + fetch('script') ?> + + +
+ +
+ Flash->render() ?> + + 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) && strlen(trim($address->Tag)) > 0): ?> + TagUrl)) > 0): ?>Tag ?>Tag; endif; ?> + Tag this 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.
+
+ +
+
+ +
+
+ +
+
+ + +
+
+
+ +
+
+ +
+
+ lbry:<?php echo $address->Address ?> +
+ +
+
+
Received (LBC)
+
Amount->format($totalReceived) ?>
+
+ +
+
Sent (LBC)
+
Amount->format($totalSent) ?>
+
+ +
+
Balance (LBC)
+
Amount->format($balanceAmount) ?>
+
+ +
+
+ +
+
+ +
+

Recent Transactions

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HeightTransaction HashTimestampConfirmationsInputsOutputsAmount
There are no recent transactions to display for this wallet.
Height === null): ?>UnconfirmedHeight ?>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 ?>

+
+ +
+ PreviousBlockHash)) > 0): ?> + « Previous Block + + + NextBlockHash)) > 0): ?> + Next Block » + + +
+
+ +
+
+

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

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
HashInputsOutputsValue
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
+
+ +
Realtime
+
+
+ +
+
+
Block Height
+
Height ?>
+
+ +
+
Difficulty
+
Difficulty, 2, '.', '') ?>
+
+ +
+
Network
+
+
+ +
+
Price
+
+
+ +
+
+ +
+

Recent Blocks

+ + + + + + + + + + + + + + + + + + + + + + + + +
HeightAgeBlock SizeTransactionsDifficultyTimestamp
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

+ + + + + + + + + + + + + + + + + + +
HeightAge# TXs
Height ?>BlockTime)->diffForHumans(); ?>TransactionCount ?>
+
+ +
+

Recent Transactions

+ + + + + + + + + + + + + + + + + + + + + + +
HashTimeInputsOutputsAmount
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.
+ + + +
+ +
+
Confirmations
+
+
+ +
+
Size (bytes)
+
TransactionSize, 0, '', ',') ?>
+
+ +
+
Inputs
+
InputCount ?>
+
+ +
+
Outputs
+
OutputCount ?>
+
+ +
+
+ +
+

Details

+
+
+
InputCount ?> inputInputCount === 1 ? '' : 's'; ?>
+ + +
+ +
Block Reward (New Coins)
+ + Value)) == 0): ?> +
Incomplete data
+ Address])): + $setAddressIds[$addr->Address] = 1; ?> + + +
Amount->format($in['Value']) ?> LBC from
+
Address ?> + (output) + Tag) && strlen(trim($addr->Tag)) > 0): ?> +
+ TagUrl)) > 0): ?>Tag ?>Tag; endif; ?> +
+ +
+ + +
+ +
+ +
+ +
+ +
+
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'; +?> + + + + Html->charset() ?> + + + <?= $cakeDescription ?> + + + Html->meta('icon') ?> + Html->css('base.css') ?> + Html->css('cake.css') ?> + Html->css('home.css') ?> + + + + +
+
Html->image('cake.logo.svg') ?>
+
+

Welcome to CakePHP Red Velvet. Build fast. Grow solid.

+
+
+ +
+
+
+

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 ).
  • + +
  • Your version of PHP is too low. You need PHP 5.6.0 or higher to use CakePHP (detected ).
  • + + + +
  • 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 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.
  • + +
+
+
+

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=/\s*$/g;function Ea(a,b){return B(a,"table")&&B(11!==b.nodeType?b:b.firstChild,"tr")?r(">tbody",a)[0]||a:a}function Fa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Ga(a){var b=Ca.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ha(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(W.hasData(a)&&(f=W.access(a),g=W.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&Ba.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ja(f,b,c,d)});if(m&&(e=qa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(na(e,"script"),Fa),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=na(h),f=na(a),d=0,e=f.length;d0&&oa(g,!i&&na(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(U(c)){if(b=c[W.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[W.expando]=void 0}c[X.expando]&&(c[X.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ka(this,a,!0)},remove:function(a){return Ka(this,a)},text:function(a){return T(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.appendChild(a)}})},prepend:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(na(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return T(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!Aa.test(a)&&!ma[(ka.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function _a(a,b,c,d,e){return new _a.prototype.init(a,b,c,d,e)}r.Tween=_a,_a.prototype={constructor:_a,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=_a.propHooks[this.prop];return a&&a.get?a.get(this):_a.propHooks._default.get(this)},run:function(a){var b,c=_a.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):_a.propHooks._default.set(this),this}},_a.prototype.init.prototype=_a.prototype,_a.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},_a.propHooks.scrollTop=_a.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=_a.prototype.init,r.fx.step={};var ab,bb,cb=/^(?:toggle|show|hide)$/,db=/queueHooks$/;function eb(){bb&&(d.hidden===!1&&a.requestAnimationFrame?a.requestAnimationFrame(eb):a.setTimeout(eb,r.fx.interval),r.fx.tick())}function fb(){return a.setTimeout(function(){ab=void 0}),ab=r.now()}function gb(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ca[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function hb(a,b,c){for(var d,e=(kb.tweeners[b]||[]).concat(kb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?lb:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b), +null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&B(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(L);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),lb={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=mb[b]||r.find.attr;mb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=mb[g],mb[g]=e,e=null!=c(a,b,d)?g:null,mb[g]=f),e}});var nb=/^(?:input|select|textarea|button)$/i,ob=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return T(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):nb.test(a.nodeName)||ob.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function pb(a){var b=a.match(L)||[];return b.join(" ")}function qb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,qb(this)))});if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,qb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,qb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(L)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=qb(this),b&&W.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":W.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+pb(qb(c))+" ").indexOf(b)>-1)return!0;return!1}});var rb=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":Array.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(rb,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:pb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(Array.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var sb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!sb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,sb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(W.get(h,"events")||{})[b.type]&&W.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&U(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!U(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=W.access(d,b);e||d.addEventListener(a,c,!0),W.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=W.access(d,b)-1;e?W.access(d,b,e):(d.removeEventListener(a,c,!0),W.remove(d,b))}}});var tb=a.location,ub=r.now(),vb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var wb=/\[\]$/,xb=/\r?\n/g,yb=/^(?:submit|button|image|reset|file)$/i,zb=/^(?:input|select|textarea|keygen)/i;function Ab(a,b,c,d){var e;if(Array.isArray(b))r.each(b,function(b,e){c||wb.test(a)?d(a,e):Ab(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)Ab(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(Array.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)Ab(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&zb.test(this.nodeName)&&!yb.test(a)&&(this.checked||!ja.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:Array.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(xb,"\r\n")}}):{name:b.name,value:c.replace(xb,"\r\n")}}).get()}});var Bb=/%20/g,Cb=/#.*$/,Db=/([?&])_=[^&]*/,Eb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Fb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Gb=/^(?:GET|HEAD)$/,Hb=/^\/\//,Ib={},Jb={},Kb="*/".concat("*"),Lb=d.createElement("a");Lb.href=tb.href;function Mb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(L)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Nb(a,b,c,d){var e={},f=a===Jb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Ob(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Pb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Qb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:tb.href,type:"GET",isLocal:Fb.test(tb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Kb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Ob(Ob(a,r.ajaxSettings),b):Ob(r.ajaxSettings,a)},ajaxPrefilter:Mb(Ib),ajaxTransport:Mb(Jb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Eb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||tb.href)+"").replace(Hb,tb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(L)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Lb.protocol+"//"+Lb.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Nb(Ib,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Gb.test(o.type),f=o.url.replace(Cb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(Bb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(vb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Db,"$1"),n=(vb.test(f)?"&":"?")+"_="+ub++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Kb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Nb(Jb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Pb(o,y,d)),v=Qb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Rb={0:200,1223:204},Sb=r.ajaxSettings.xhr();o.cors=!!Sb&&"withCredentials"in Sb,o.ajax=Sb=!!Sb,r.ajaxTransport(function(b){var c,d;if(o.cors||Sb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Rb[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r("