diff --git a/config/routes.php b/config/routes.php index e34d2c1..4b74090 100644 --- a/config/routes.php +++ b/config/routes.php @@ -50,6 +50,7 @@ Router::scope('/', function (RouteBuilder $routes) { $routes->connect('/claims/*', ['controller' => 'Main', 'action' => 'claims']); $routes->connect('/find', ['controller' => 'Main', 'action' => 'find']); $routes->connect('/realtime', ['controller' => 'Main', 'action' => 'realtime']); + $routes->connect('/stats', ['controller' => 'Main', 'action' => 'stats']); $routes->connect('/tx/*', ['controller' => 'Main', 'action' => 'tx']); $routes->connect('/qr/*', ['controller' => 'Main', 'action' => 'qr']); diff --git a/sql/lbryexplorer.ddl.sql b/sql/lbryexplorer.ddl.sql index 53f6ab4..10b8f69 100644 --- a/sql/lbryexplorer.ddl.sql +++ b/sql/lbryexplorer.ddl.sql @@ -73,6 +73,7 @@ CREATE TABLE `Addresses` `FirstSeen` DATETIME, `TotalReceived` DECIMAL(18,8) DEFAULT 0 NOT NULL, `TotalSent` DECIMAL(18,8) DEFAULT 0 NOT NULL, + `Balance` DECIMAL(18,8) AS (`TotalReceived` - `TotalSent`) PERSISTENT, `Tag` VARCHAR(30) NOT NULL, `TagUrl` VARCHAR(200), `Created` DATETIME NOT NULL, @@ -82,6 +83,7 @@ CREATE TABLE `Addresses` UNIQUE KEY `Idx_AddressTag` (`Tag`), INDEX `Idx_AddressTotalReceived` (`TotalReceived`), INDEX `Idx_AddressTotalSent` (`TotalSent`), + INDEX `Idx_AddressBalance` (`Balance`), INDEX `Idx_AddressCreated` (`Created`), INDEX `Idx_AddressModified` (`Modified`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=4; @@ -232,4 +234,6 @@ CREATE TABLE `PriceHistory` UNIQUE KEY `Idx_PriceHistoryCreated` (`Created`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=4; -ALTER TABLE Claims ADD UNIQUE KEY `Idx_ClaimUnique` (`TransactionHash`, `Vout`, `ClaimId`); \ No newline at end of file +ALTER TABLE Claims ADD UNIQUE KEY `Idx_ClaimUnique` (`TransactionHash`, `Vout`, `ClaimId`); +ALTER TABLE Addresses ADD COLUMN `Balance` DECIMAL(18,8) AS (`TotalReceived` - `TotalSent`) PERSISTENT AFTER `TotalSent`; +ALTER TABLE Addresses ADD INDEX `Idx_AddressBalance` (`Balance`); \ No newline at end of file diff --git a/src/Controller/MainController.php b/src/Controller/MainController.php index 2be899c..040bae3 100644 --- a/src/Controller/MainController.php +++ b/src/Controller/MainController.php @@ -491,6 +491,37 @@ class MainController extends AppController { $this->set('sourceAddress', $sourceAddress); } + public function stats() { + $this->loadModel('Addresses'); + + $richList = $this->Addresses->find()->order(['Balance' => 'DESC'])->limit(500)->toArray(); + + $priceRate = 0; + $priceInfo = json_decode($this->redis->get(self::lbcPriceKey)); + if (isset($priceInfo->price)) { + $priceRate = $priceInfo->price; + } + + // calculate percentages + $totalBalance = 0; + $maxBalance = 0; + $minBalance = 0; + foreach ($richList as $item) { + $totalBalance = bcadd($totalBalance, $item->Balance, 8); + $minBalance = $minBalance == 0 ? $item->Balance : min($minBalance, $item->Balance); + $maxBalance = max($maxBalance, $item->Balance); + } + for ($i = 0; $i < count($richList); $i++) { + $item = $richList[$i]; + $percentage = bcdiv($item->Balance, $totalBalance, 8) * 100; + $richList[$i]->Top500Percent = $percentage; + $richList[$i]->MinMaxPercent = bcdiv($item->Balance, $maxBalance, 8) * 100; + } + + $this->set('richList', $richList); + $this->set('rate', $priceRate); + } + public function address($addr = null) { set_time_limit(0); @@ -558,7 +589,7 @@ class MainController extends AppController { $offset = ($page - 1) * $pageLimit; } - $stmt = $conn->execute('SELECT A.TotalReceived, A.TotalSent FROM Addresses A WHERE A.Id = ?', [$address->Id]); + $stmt = $conn->execute('SELECT A.TotalReceived, A.TotalSent, A.Balance FROM Addresses A WHERE A.Id = ?', [$address->Id]); $totals = $stmt->fetch(\PDO::FETCH_OBJ); $stmt = $conn->execute(sprintf('SELECT T.Id, T.Hash, T.InputCount, T.OutputCount, T.Value, ' . @@ -572,7 +603,7 @@ class MainController extends AppController { $totalRecvAmount = $totals->TotalReceived == 0 ? '0' : $totals->TotalReceived + 0; $totalSentAmount = $totals->TotalSent == 0 ? '0' : $totals->TotalSent + 0; - $balanceAmount = bcsub($totalRecvAmount, $totalSentAmount, 8) + 0; + $balanceAmount = $totals->Balance == 0 ? '0' : $totals->Balance + 0; } $this->set('offset', $offset); diff --git a/src/Template/Main/index.ctp b/src/Template/Main/index.ctp index 2d69dc0..e94b52b 100644 --- a/src/Template/Main/index.ctp +++ b/src/Template/Main/index.ctp @@ -116,7 +116,7 @@
LBRY Block Explorer
-
Realtime
+
diff --git a/src/Template/Main/stats.ctp b/src/Template/Main/stats.ctp new file mode 100644 index 0000000..84d668f --- /dev/null +++ b/src/Template/Main/stats.ctp @@ -0,0 +1,53 @@ +assign('title', 'Stats & Rich List') ?> + +start('script'); ?> + +end(); ?> + +element('header') ?> + +
+

LBRY Stats

+
+ +
+ +
+

LBRY Rich List (Top 500)

+ + + + + + + + + + + + + + + + + + + + + + + + +
RankAddressBalance (LBC)Balance (USD)First Seen% Top 500
Address ?> + Tag) && strlen(trim($item->Tag)) > 0): ?> +
+ TagUrl)) > 0): ?>Tag ?>Tag; endif; ?> +
+
Balance, 8, '.', ',') ?>$Balance, $rate, 8), 2, '.', ',') ?>FirstSeen->format('d M Y H:i:s') . ' UTC'; ?>
Top500Percent, 2, '.', '') ?>%
+
+ +
+ +
diff --git a/webroot/css/main.css b/webroot/css/main.css index 75ba6d4..79ccec4 100644 --- a/webroot/css/main.css +++ b/webroot/css/main.css @@ -21,7 +21,9 @@ border-radius: 0 6px 8px 0 } .home-container-cell .ctls { width: 720px; 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 .links { display: inline-block; font-weight: 300; position: absolute; right: 0; padding-top: 12px; } +.home-container-cell .ctls a { font-size: 115%; display: inline-block; margin-right: 12px } +.home-container-cell .ctls a.last { margin-left: 12px; margin-right: 0 } .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 2px 6px rgba(0,0,0,.175); border: 1px solid rgba(0,0,0,.15); padding: 24px 24px 36px 24px; cursor: default; position: relative } @@ -122,14 +124,17 @@ border-radius: 0 6px 8px 0 } .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 .topvalign { vertical-align: top } .table .last-cell { padding-left: 48px } .table .pad-right { padding-right: 12px } .table .pad-left { padding-left: 48px } +.table .med-pad-left { padding-left: 24px } .left { text-align: left } .center { text-align: center } .right { text-align: right } +.w50 { width: 50px } .w80 { width: 80px } .w100 { width: 100px } .w125 { width: 125px } @@ -139,6 +144,7 @@ border-radius: 0 6px 8px 0 } .w250, .w250 > div { width: 250px } .w275, .w275 > div { width: 275px } .w300, .w300 > div { width: 300px } +.w350 { width: 350px } .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 } @@ -196,6 +202,17 @@ footer .content .page-time { position: absolute; right: 12px; bottom: 0px; paddi .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-head { width: 1200px; margin: 0 auto 36px auto; cursor: default } +.stats-head h3 { font-weight: 300; margin: 0; font-size: 200% } +.stats-main .richlist { width: 1200px; margin: 0 auto; box-shadow: 0 2px 6px rgba(0,0,0,.175); border: 1px solid rgba(0,0,0,.15); padding: 24px; cursor: default } +.stats-main .richlist .tag { font-weight: normal; margin-top: 1px; font-size: 80%; color: #333 } +.stats-main .richlist .tag a:link, .tx-details-layout .tag a:visited { color: #333 } +.stats-main .richlist .tag a:hover { text-decoration: none } +.stats-main .richlist .top500-percent-cell { padding: 0; position: relative } +.stats-main .richlist .top500-percent { background: #e3f2fd; height: 100%; position: absolute; top: 0 } +.stats-main .richlist .top500-percent-cell .text { z-index: 100; position: relative } +.stats-main h3 { font-weight: 300; margin: 0 0 12px 0 } + .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% }