├── .gitignore ├── LICENSE ├── README.org ├── grinexplorer ├── blockchain │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── management │ │ └── commands │ │ │ └── import_from_tip.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20180321_1420.py │ │ ├── 0003_remove_output_switch_commit_hash.py │ │ ├── 0004_remove_block_difficulty.py │ │ ├── 0005_block_total_kernel_offset.py │ │ ├── 0006_block_cuckoo_size.py │ │ ├── 0007_block_cuckoo_solution.py │ │ ├── 0008_output_block_height.py │ │ ├── 0009_auto_20181018_1931.py │ │ ├── 0010_auto_20181030_1436.py │ │ ├── 0011_auto_20181113_1746.py │ │ ├── 0012_output_mmr_index.py │ │ ├── 0013_auto_20190521_2016.py │ │ ├── 0014_auto_20191202_1529.py │ │ ├── 0015_auto_20200414_0922.py │ │ ├── 0016_auto_20200716_1100.py │ │ ├── 0017_kernel_fee_shift.py │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ └── views.py ├── explorer │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── charts.py │ ├── models.py │ ├── templates │ │ └── explorer │ │ │ ├── block_chart.html │ │ │ ├── block_detail.html │ │ │ ├── block_list.html │ │ │ ├── blocks_by_height.html │ │ │ ├── fee_chart.html │ │ │ ├── output_detail.html │ │ │ └── search_results.html │ ├── templatetags │ │ ├── grin.py │ │ └── shortnaturaltime.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── grinexplorer │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── manage.py ├── static │ ├── css │ │ ├── project.css │ │ └── style.css │ ├── images │ │ ├── favicon-32x32.png │ │ ├── favicon-96x96.png │ │ ├── favicon.ico │ │ └── icons │ │ │ └── logo_80x80.png │ └── webfonts │ │ ├── LICENSE.txt │ │ ├── fa-brands-400.eot │ │ ├── fa-brands-400.svg │ │ ├── fa-brands-400.ttf │ │ ├── fa-brands-400.woff │ │ ├── fa-brands-400.woff2 │ │ ├── fa-regular-400.eot │ │ ├── fa-regular-400.svg │ │ ├── fa-regular-400.ttf │ │ ├── fa-regular-400.woff │ │ ├── fa-regular-400.woff2 │ │ ├── fa-solid-900.eot │ │ ├── fa-solid-900.svg │ │ ├── fa-solid-900.ttf │ │ ├── fa-solid-900.woff │ │ └── fa-solid-900.woff2 └── templates │ └── base.html └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Add any directories, files, or patterns you don't want to be tracked by version control 2 | \#* 3 | *.pyc 4 | *~ 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published by 637 | the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * Grin-Explorer 2 | 3 | Grin-Explorer is the first block explorer for the [[https://grin.mw][Grin]] 4 | blockchain. This is the source code for the instance running at 5 | [[https://grinexplorer.net]]. 6 | 7 | I hope it provides a useful service for anybody interested in Grin and to its 8 | development. 9 | 10 | ** Bugs, Feature Requests, etc. 11 | 12 | Please report bugs, feature requests or feedback 13 | [[https://github.com/mimblewimble/grin-explorer/issues/new][here]] or ping 14 | me (@hendi) on Gitter. 15 | 16 | ** License 17 | 18 | grin-explorer is licensed under the terms of the GNU AGPL 3, see the file 19 | [[LICENSE]] for details. Basically it allows you to use and modify this code as 20 | long as you make all changes available under the same terms. The easiest way to 21 | do this is to fork this repo (publically!) and deploy your instance from 22 | that repo.p 23 | 24 | ** How to setup your own instance 25 | 26 | *** Requirements 27 | 28 | Python >= 3.5 and PostgreSQL >= 9.3 are required. 29 | 30 | Grin is required to run as an archival node and basic auth needs to be disabled 31 | in its REST API. Set `archive_mode = true` and comment out `api_secret_path` in 32 | `grin-server.toml` respectively. 33 | 34 | *** Installation 35 | 36 | Start PostgreSQL. Note that the following is just an example to run PostgreSQL, 37 | you should ensure you use your own user and credentials when setting it up. 38 | 39 | #+begin_src sh 40 | sudo postgresql-setup --initdb 41 | sudo systemctl start postgresql 42 | sudo -u postgres -i 43 | # psql 44 | postgres=# \password postgres 45 | Enter new password: 46 | Enter it again: 47 | postgres=# CREATE USER myuser WITH PASSWORD 'mypass'; 48 | postgres=# CREATE DATABASE mydb; 49 | postgres=# GRANT ALL PRIVILEGES ON DATABASE mydb to myuser; 50 | postgres=# \q 51 | sudo systemctl restart postgresql 52 | #+end_src 53 | 54 | Start `grin-explorer`. Again, ensure you use your own credentials for Django. 55 | 56 | #+begin_src sh 57 | git clone https://github.com/mimblewimble/grin-explorer 58 | cd grin-explorer 59 | python3 -m venv venv 60 | source venv/bin/activate 61 | pip install -r requirements.txt 62 | # SECRET_KEY is required by Django 63 | export SECRET_KEY=somesecretkey 64 | # PostgreSQL configuration 65 | export DB_NAME=mydb 66 | export DB_USER=myuser 67 | export DB_PASSWORD=mypass 68 | export DB_HOST=127.0.0.1 69 | export DB_PORT=5432 70 | python3 ./grinexplorer/manage.py migrate 71 | python3 ./grinexplorer/manage.py runserver 72 | #+end_src 73 | 74 | *** Import blockchain 75 | 76 | #+begin_src sh 77 | python3 ./grinexplorer/manage.py import_from_tip http://127.0.0.1:13413 78 | #+end_src 79 | 80 | -------------------------------------------------------------------------------- /grinexplorer/blockchain/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-explorer/59b66966561e6e522807dc272a96ea72f69d9524/grinexplorer/blockchain/__init__.py -------------------------------------------------------------------------------- /grinexplorer/blockchain/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /grinexplorer/blockchain/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BlockchainConfig(AppConfig): 5 | name = 'blockchain' 6 | -------------------------------------------------------------------------------- /grinexplorer/blockchain/management/commands/import_from_tip.py: -------------------------------------------------------------------------------- 1 | import json 2 | from enum import Enum 3 | 4 | import requests 5 | 6 | from django.core.management.base import BaseCommand 7 | 8 | from blockchain.models import Block, Input, Output, Kernel 9 | 10 | 11 | class Status(Enum): 12 | CREATED = 0 13 | ALREADY_EXISTS = 1 14 | 15 | 16 | class Command(BaseCommand): 17 | help = "Import the Blockchain starting from the current tip" 18 | 19 | def rpc(self, method, **params): 20 | resp = requests.post( 21 | self.API_BASE, 22 | json={ 23 | "id": "json", 24 | "method": method, 25 | "params": params, 26 | }) 27 | 28 | return resp.json()["result"]["Ok"] 29 | 30 | def add_arguments(self, parser): 31 | parser.add_argument("url", type=str) 32 | parser.add_argument( 33 | '--full-scan', 34 | dest='full-scan', 35 | default=False, 36 | help="Continue scanning even if Block already exists", 37 | ) 38 | 39 | def handle(self, *args, **options): 40 | self.API_BASE = "%s/v2/foreign" % options["url"] 41 | 42 | try: 43 | data = self.rpc("get_tip") 44 | except json.decoder.JSONDecodeError: 45 | print("Decoding JSON failed (make sure to disable api_secret_path in grin-server.toml") 46 | print("resp=%r" % resp.text) 47 | exit() 48 | 49 | height = data["height"] 50 | tip = data["last_block_pushed"] 51 | self.stdout.write("height={}, tip={}\n\n".format(height, tip)) 52 | 53 | hash = tip 54 | parent = None 55 | while True: 56 | (status, block_hash, prev_hash) = self.fetch_and_store_block(hash, parent) 57 | 58 | if not options["full-scan"] and status == Status.ALREADY_EXISTS: 59 | self.stdout.write("== exiting early") 60 | break 61 | 62 | if prev_hash == "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff": 63 | break 64 | 65 | # continue along the chain 66 | hash = prev_hash 67 | parent = block_hash 68 | 69 | def fetch_and_store_block(self, hash, parent_hash): 70 | try: 71 | block_data = self.rpc("get_block", hash=hash, height=None, commit=None) 72 | except json.decoder.JSONDecodeError: 73 | print("Decoding JSON failed (make sure to set `archive_mode=true` in grin-server.toml") 74 | print("resp=%r" % resp.text) 75 | exit() 76 | 77 | try: 78 | block = Block.objects.get(hash=hash) 79 | self.stdout.write("Block {} already exists @ {}".format( 80 | block.hash, block.height)) 81 | 82 | if parent_hash is not None: 83 | parent = Block.objects.get(hash=parent_hash) 84 | 85 | if parent.previous is None: 86 | assert parent.height == block.height + 1 87 | parent.previous = block 88 | parent.save(update_fields=["previous"]) 89 | self.stdout.write("Stored block {} as previous of block {}".format( 90 | block.hash[:6], parent.hash[:6])) 91 | 92 | return (Status.ALREADY_EXISTS, block.hash, block_data["header"]["previous"]) 93 | except Block.DoesNotExist: 94 | pass 95 | 96 | assert hash == block_data["header"]["hash"] 97 | 98 | # we can't set the `previous` PK now because it doesn't exist yet. We'll 99 | # set it in the next call of this function, when the previous Block is saved 100 | previous = block_data["header"].pop("previous") 101 | 102 | block = Block.objects.create( 103 | previous=None, 104 | **block_data["header"], 105 | ) 106 | for input_data in block_data["inputs"]: 107 | Input.objects.create( 108 | block=block, 109 | data=input_data, 110 | ) 111 | 112 | for output_data in block_data["outputs"]: 113 | Output.objects.create( 114 | block=block, 115 | **output_data, 116 | ) 117 | 118 | for kernel_data in block_data["kernels"]: 119 | Kernel.objects.create( 120 | block=block, 121 | **kernel_data, 122 | ) 123 | 124 | self.stdout.write("Stored block {} @ {}".format( 125 | block.hash, block.height)) 126 | # set parent's `previous` PK to this Block 127 | if parent_hash is not None: 128 | parent = Block.objects.get(hash=parent_hash) 129 | assert parent.height == block.height + 1 130 | parent.previous = block 131 | parent.save(update_fields=["previous"]) 132 | self.stdout.write(" Marked block {} as previous of block {}".format( 133 | block.hash[:6], parent.hash[:6])) 134 | 135 | return (Status.CREATED, block.hash, previous) 136 | -------------------------------------------------------------------------------- /grinexplorer/blockchain/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0 on 2017-12-14 08:58 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Block', 17 | fields=[ 18 | ('hash', models.CharField(db_index=True, max_length=64, primary_key=True, serialize=False)), 19 | ('version', models.IntegerField()), 20 | ('height', models.IntegerField(db_index=True)), 21 | ('timestamp', models.DateTimeField()), 22 | ('utxo_root', models.CharField(max_length=64)), 23 | ('range_proof_root', models.CharField(max_length=64)), 24 | ('kernel_root', models.CharField(max_length=64)), 25 | ('nonce', models.TextField()), 26 | ('difficulty', models.IntegerField()), 27 | ('total_difficulty', models.IntegerField()), 28 | ('previous', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='children_set', to='blockchain.Block')), 29 | ], 30 | ), 31 | migrations.CreateModel( 32 | name='Input', 33 | fields=[ 34 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 35 | ('data', models.CharField(max_length=66)), 36 | ('block', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='blockchain.Block')), 37 | ], 38 | ), 39 | migrations.CreateModel( 40 | name='Kernel', 41 | fields=[ 42 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 43 | ('features', models.TextField()), 44 | ('fee', models.IntegerField()), 45 | ('lock_height', models.IntegerField()), 46 | ('excess', models.CharField(max_length=66)), 47 | ('excess_sig', models.CharField(max_length=142)), 48 | ('block', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='blockchain.Block')), 49 | ], 50 | ), 51 | migrations.CreateModel( 52 | name='Output', 53 | fields=[ 54 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 55 | ('output_type', models.TextField(choices=[('Transaction', 'Transaction'), ('Coinbase', 'Coinbase')])), 56 | ('commit', models.CharField(max_length=66)), 57 | ('switch_commit_hash', models.CharField(max_length=40)), 58 | ('height', models.IntegerField()), 59 | ('lock_height', models.IntegerField()), 60 | ('spent', models.BooleanField()), 61 | ('proof_hash', models.CharField(max_length=64)), 62 | ('block', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='blockchain.Block')), 63 | ], 64 | ), 65 | ] 66 | -------------------------------------------------------------------------------- /grinexplorer/blockchain/migrations/0002_auto_20180321_1420.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0 on 2018-03-21 14:20 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('blockchain', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='block', 15 | old_name='utxo_root', 16 | new_name='output_root', 17 | ), 18 | migrations.RemoveField( 19 | model_name='output', 20 | name='height', 21 | ), 22 | migrations.RemoveField( 23 | model_name='output', 24 | name='lock_height', 25 | ), 26 | migrations.AddField( 27 | model_name='output', 28 | name='merkle_proof', 29 | field=models.TextField(null=True), 30 | ), 31 | migrations.AddField( 32 | model_name='output', 33 | name='proof', 34 | field=models.TextField(null=True), 35 | ), 36 | migrations.AlterField( 37 | model_name='output', 38 | name='switch_commit_hash', 39 | field=models.CharField(max_length=64), 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /grinexplorer/blockchain/migrations/0003_remove_output_switch_commit_hash.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0 on 2018-03-26 14:52 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('blockchain', '0002_auto_20180321_1420'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='output', 15 | name='switch_commit_hash', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /grinexplorer/blockchain/migrations/0004_remove_block_difficulty.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0 on 2018-03-26 14:56 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('blockchain', '0003_remove_output_switch_commit_hash'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='block', 15 | name='difficulty', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /grinexplorer/blockchain/migrations/0005_block_total_kernel_offset.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0 on 2018-04-19 10:32 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('blockchain', '0004_remove_block_difficulty'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='block', 15 | name='total_kernel_offset', 16 | field=models.CharField(default='', max_length=64), 17 | preserve_default=False, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /grinexplorer/blockchain/migrations/0006_block_cuckoo_size.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0 on 2018-07-10 04:58 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('blockchain', '0005_block_total_kernel_offset'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='block', 15 | name='cuckoo_size', 16 | field=models.IntegerField(default=30), 17 | preserve_default=False, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /grinexplorer/blockchain/migrations/0007_block_cuckoo_solution.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0 on 2018-07-16 10:01 2 | 3 | import django.contrib.postgres.fields 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('blockchain', '0006_block_cuckoo_size'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='block', 16 | name='cuckoo_solution', 17 | field=django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), default=[], size=None), 18 | preserve_default=False, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /grinexplorer/blockchain/migrations/0008_output_block_height.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0 on 2018-09-03 10:56 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('blockchain', '0007_block_cuckoo_solution'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='output', 15 | name='block_height', 16 | field=models.IntegerField(null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /grinexplorer/blockchain/migrations/0009_auto_20181018_1931.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.2 on 2018-10-18 19:31 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('blockchain', '0008_output_block_height'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='block', 15 | old_name='cuckoo_size', 16 | new_name='edge_bits', 17 | ), 18 | migrations.AddField( 19 | model_name='block', 20 | name='prev_root', 21 | field=models.CharField(default=29, max_length=64), 22 | preserve_default=False, 23 | ), 24 | migrations.AddField( 25 | model_name='block', 26 | name='scaling_difficulty', 27 | field=models.IntegerField(default=29), 28 | preserve_default=False, 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /grinexplorer/blockchain/migrations/0010_auto_20181030_1436.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.1 on 2018-10-30 14:36 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('blockchain', '0009_auto_20181018_1931'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='block', 15 | old_name='scaling_difficulty', 16 | new_name='secondary_scaling', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /grinexplorer/blockchain/migrations/0011_auto_20181113_1746.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.1 on 2018-11-13 17:46 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('blockchain', '0010_auto_20181030_1436'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='block', 15 | name='total_difficulty', 16 | field=models.BigIntegerField(), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /grinexplorer/blockchain/migrations/0012_output_mmr_index.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.1 on 2019-01-19 02:52 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('blockchain', '0011_auto_20181113_1746'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='output', 15 | name='mmr_index', 16 | field=models.IntegerField(null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /grinexplorer/blockchain/migrations/0013_auto_20190521_2016.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.1 on 2019-05-21 20:16 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('blockchain', '0012_output_mmr_index'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='input', 15 | name='data', 16 | field=models.CharField(db_index=True, max_length=66), 17 | ), 18 | migrations.AlterField( 19 | model_name='output', 20 | name='commit', 21 | field=models.CharField(db_index=True, max_length=66), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /grinexplorer/blockchain/migrations/0014_auto_20191202_1529.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.1 on 2019-12-02 15:29 2 | 3 | import django.contrib.postgres.fields 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('blockchain', '0013_auto_20190521_2016'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='block', 16 | name='cuckoo_solution', 17 | field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(), size=None), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /grinexplorer/blockchain/migrations/0015_auto_20200414_0922.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.1 on 2020-04-14 09:22 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('blockchain', '0014_auto_20191202_1529'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='block', 15 | name='timestamp', 16 | field=models.DateTimeField(db_index=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /grinexplorer/blockchain/migrations/0016_auto_20200716_1100.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.1 on 2020-07-16 11:00 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('blockchain', '0015_auto_20200414_0922'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='block', 15 | name='kernel_mmr_size', 16 | field=models.IntegerField(default=0), 17 | preserve_default=False, 18 | ), 19 | migrations.AddField( 20 | model_name='block', 21 | name='output_mmr_size', 22 | field=models.IntegerField(default=0), 23 | preserve_default=False, 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /grinexplorer/blockchain/migrations/0017_kernel_fee_shift.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.1 on 2020-12-14 14:53 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('blockchain', '0016_auto_20200716_1100'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='kernel', 15 | name='fee_shift', 16 | field=models.IntegerField(default=0), 17 | preserve_default=False, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /grinexplorer/blockchain/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-explorer/59b66966561e6e522807dc272a96ea72f69d9524/grinexplorer/blockchain/migrations/__init__.py -------------------------------------------------------------------------------- /grinexplorer/blockchain/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.postgres.fields import ArrayField 3 | 4 | SECOND_POW_EDGE_BITS = 29 5 | BASE_EDGE_BITS = 24 6 | 7 | 8 | def graph_weight(edge_bits): 9 | # Compute weight of a graph as number of siphash bits defining the graph 10 | return (2 << edge_bits-BASE_EDGE_BITS)*edge_bits 11 | 12 | 13 | def scaled_difficulty(hash, graph_weight): 14 | # Difficulty achieved by this proof with given scaling factor 15 | diff = ((graph_weight) << 64) / int(hash[:16], 16) 16 | return min(diff, 0xffffffffffffffff) 17 | 18 | 19 | def from_proof_adjusted(hash, edge_bits): 20 | # Computes the difficulty from a hash. Divides the maximum target by the 21 | # provided hash and applies the Cuck(at)oo size adjustment factor 22 | # scale with natural scaling factor 23 | return scaled_difficulty(hash, graph_weight(edge_bits)) 24 | 25 | 26 | def from_proof_scaled(hash, secondary_scaling): 27 | # Same as `from_proof_adjusted` but instead of an adjustment based on 28 | # cycle size, scales based on a provided factor. Used by dual PoW system 29 | # to scale one PoW against the other. 30 | # Scaling between 2 proof of work algos 31 | return scaled_difficulty(hash, secondary_scaling) 32 | 33 | 34 | class Block(models.Model): 35 | hash = models.CharField( 36 | max_length=64, 37 | db_index=True, 38 | primary_key=True, 39 | ) 40 | 41 | version = models.IntegerField() 42 | 43 | height = models.IntegerField( 44 | db_index=True, 45 | ) 46 | 47 | previous = models.ForeignKey( 48 | related_name="children_set", 49 | to="Block", 50 | on_delete=models.PROTECT, 51 | db_index=True, 52 | null=True, 53 | ) 54 | 55 | prev_root = models.CharField(max_length=64) 56 | 57 | timestamp = models.DateTimeField( 58 | db_index=True, 59 | ) 60 | 61 | output_root = models.CharField(max_length=64) 62 | 63 | range_proof_root = models.CharField(max_length=64) 64 | 65 | kernel_root = models.CharField(max_length=64) 66 | 67 | nonce = models.TextField() 68 | 69 | edge_bits = models.IntegerField() 70 | 71 | cuckoo_solution = ArrayField(models.BigIntegerField()) 72 | 73 | difficulty = models.BigIntegerField() 74 | 75 | # sum of the target difficulties, not the sum of the actual block difficulties 76 | total_difficulty = models.BigIntegerField() 77 | 78 | secondary_scaling = models.IntegerField() 79 | 80 | total_kernel_offset = models.CharField(max_length=64) 81 | 82 | output_mmr_size = models.IntegerField() 83 | 84 | kernel_mmr_size = models.IntegerField() 85 | 86 | @property 87 | def difficulty(self): 88 | # Maximum difficulty this proof of work can achieve 89 | # 2 proof of works, Cuckoo29 (for now) and Cuckoo30+, which are scaled 90 | # differently (scaling not controlled for now) 91 | if (self.edge_bits == SECOND_POW_EDGE_BITS): 92 | return int(from_proof_scaled(self.hash, self.secondary_scaling)) 93 | else: 94 | return int(from_proof_adjusted(self.hash, self.edge_bits)) 95 | 96 | @property 97 | def target_difficulty(self): 98 | if self.previous is None: 99 | return None 100 | return self.total_difficulty - self.previous.total_difficulty 101 | 102 | @property 103 | def reward(self): 104 | return 60 105 | 106 | @property 107 | def fees(self): 108 | return sum([x.fee for x in self.kernel_set.all()]) 109 | 110 | 111 | class Input(models.Model): 112 | block = models.ForeignKey( 113 | to=Block, 114 | on_delete=models.PROTECT, 115 | db_index=True, 116 | ) 117 | 118 | data = models.CharField( 119 | max_length=66, 120 | db_index=True, 121 | ) 122 | 123 | 124 | class Output(models.Model): 125 | OUTPUT_TYPE = ( 126 | ("Transaction", "Transaction"), 127 | ("Coinbase", "Coinbase"), 128 | ) 129 | 130 | block = models.ForeignKey( 131 | to=Block, 132 | on_delete=models.PROTECT, 133 | db_index=True, 134 | ) 135 | 136 | output_type = models.TextField( 137 | choices=OUTPUT_TYPE 138 | ) 139 | 140 | commit = models.CharField( 141 | max_length=66, 142 | db_index=True, 143 | ) 144 | 145 | spent = models.BooleanField() 146 | 147 | proof = models.TextField(null=True) 148 | 149 | proof_hash = models.CharField(max_length=64) 150 | 151 | block_height = models.IntegerField(null=True) 152 | 153 | merkle_proof = models.TextField(null=True) 154 | 155 | mmr_index = models.IntegerField(null=True) 156 | 157 | def occurrences(self): 158 | return 1 159 | 160 | def spent_at(self): 161 | return None 162 | 163 | 164 | class Kernel(models.Model): 165 | block = models.ForeignKey( 166 | to=Block, 167 | on_delete=models.PROTECT, 168 | db_index=True, 169 | ) 170 | 171 | features = models.TextField() 172 | 173 | fee = models.IntegerField() 174 | 175 | fee_shift = models.IntegerField() 176 | 177 | lock_height = models.IntegerField() 178 | 179 | excess = models.CharField(max_length=66) 180 | 181 | excess_sig = models.CharField(max_length=142) 182 | -------------------------------------------------------------------------------- /grinexplorer/blockchain/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /grinexplorer/blockchain/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /grinexplorer/explorer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-explorer/59b66966561e6e522807dc272a96ea72f69d9524/grinexplorer/explorer/__init__.py -------------------------------------------------------------------------------- /grinexplorer/explorer/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /grinexplorer/explorer/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ExplorerConfig(AppConfig): 5 | name = 'explorer' 6 | -------------------------------------------------------------------------------- /grinexplorer/explorer/charts.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Count, Max, Min 2 | from django.db.models.functions import TruncDay 3 | from django.shortcuts import render_to_response 4 | 5 | from blockchain.models import Block, Output 6 | from chartit import DataPool, Chart 7 | 8 | 9 | def block_chart(_): 10 | blockpivotdata = DataPool( 11 | series=[{ 12 | 'options': { 13 | 'source': Block.objects.raw("select 1 as hash, " 14 | "max(total_difficulty) as total_difficulty, " 15 | "date(DATE_TRUNC('day', timestamp)) as date, count(hash) as blocks " 16 | "from blockchain_block " 17 | "group by DATE_TRUNC('day', timestamp) order by date") 18 | }, 19 | 'terms': [ 20 | 'date', 21 | 'blocks', 22 | 'total_difficulty', 23 | ] 24 | }] 25 | ) 26 | 27 | blockpivcht = Chart( 28 | datasource=blockpivotdata, 29 | series_options=[{ 30 | 'options': { 31 | 'type': 'line', 32 | 'xAxis': 0, 33 | 'yAxis': 0, 34 | 'zIndex': 1, 35 | 'legendIndex': 1, 36 | }, 37 | 'terms': { 38 | 'date': ['blocks'] 39 | }}, { 40 | 'options': { 41 | 'type': 'line', 42 | 'xAxis': 1, 43 | 'yAxis': 1, 44 | 'legendIndex': 0, 45 | }, 46 | 'terms': { 47 | 'date': ['total_difficulty'] 48 | } 49 | }], 50 | chart_options={ 51 | 'title': { 52 | 'text': 'Blocks and Total Difficulty' 53 | }, 54 | 'xAxis': [ 55 | { 56 | 'title': { 57 | 'text': 'Date', 58 | }, 59 | 'labels': { 60 | 'enabled': True 61 | } 62 | }, 63 | { 64 | 'title': { 65 | 'text': 'Date', 66 | 'style': { 67 | 'display': 'none' 68 | } 69 | }, 70 | 'labels': { 71 | 'enabled': False 72 | }, 73 | 'lineColor': 'transparent', 74 | 'tickLength': 0, 75 | } 76 | ], 77 | 'yAxis': [ 78 | { 79 | 'title': { 80 | 'text': 'Blocks', 81 | 'style': { 82 | 'color': '#70b3ef' 83 | } 84 | }, 85 | 'labels': { 86 | 'enabled': True 87 | }, 88 | }, 89 | { 90 | 'title': { 91 | 'text': 'Total Difficulty' 92 | }, 93 | 'labels': { 94 | 'enabled': True 95 | } 96 | } 97 | ], 98 | 'legend': { 99 | 'enabled': True 100 | }, 101 | } 102 | ) 103 | return render_to_response('explorer/block_chart.html', {'blockchart': blockpivcht}) 104 | 105 | 106 | def fee_chart(_): 107 | feepivotdata = DataPool( 108 | series=[{ 109 | 'options': { 110 | 'source': Block.objects.raw("select 1 as hash, " 111 | "date(DATE_TRUNC('day', timestamp)) as date, sum(fee)/1000000 as fee " 112 | "from blockchain_block t1 join blockchain_kernel t2 " 113 | "on t2.block_id=t1.hash " 114 | "group by DATE_TRUNC('day', timestamp) order by date") 115 | }, 116 | 'terms': [ 117 | 'date', 118 | 'fee' 119 | ] 120 | }] 121 | ) 122 | 123 | feepivcht = Chart( 124 | datasource=feepivotdata, 125 | series_options=[{ 126 | 'options': { 127 | 'type': 'line', 128 | 'stacking': False 129 | }, 130 | 'terms': { 131 | 'date': [ 132 | 'fee', 133 | ] 134 | } 135 | }], 136 | chart_options={ 137 | 'title': { 138 | 'text': 'Transaction Fee Chart'}, 139 | 'xAxis': { 140 | 'title': { 141 | 'text': 'Date'}}, 142 | 'yAxis': { 143 | 'title': { 144 | 'text': 'Tx Fee'}}, 145 | 'legend': { 146 | 'enabled': False}, 147 | 'credits': { 148 | 'enabled': False}}, 149 | ) 150 | return render_to_response('explorer/fee_chart.html', {'feechart': feepivcht}) 151 | -------------------------------------------------------------------------------- /grinexplorer/explorer/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /grinexplorer/explorer/templates/explorer/block_chart.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block chart_loader_header %} 5 | 6 | 7 | 8 | 9 | 10 | {% load chartit %} 11 | {{ blockchart|load_charts:"containerblockchart" }} 12 | {% endblock %} 13 | 14 | 15 | {% block content %} 16 |
17 | 18 |
19 |
20 |
21 |
22 |
23 |
24 | 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /grinexplorer/explorer/templates/explorer/block_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load static %} 4 | {% load humanize %} 5 | {% load grin %} 6 | 7 | {% block content %} 8 |
9 |
10 | 11 |
12 |
13 |

14 |   Block {{ blk.height | intcomma }}

15 |
16 |
17 | 18 |
19 |
20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | {% if blk.output_mmr_size %} 76 | 77 | 78 | 79 | 80 | {% endif %} 81 | {% if blk.kernel_mmr_size %} 82 | 83 | 84 | 85 | 86 | {% endif %} 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 |
Hash{{ blk.hash }}
Version{{ blk.version }}
Previous Block 33 | {% if blk.height == 0 %} 34 | (None, this is the Genesis Block) 35 | {% else %} 36 | {{ blk.previous.hash }} 38 | {% endif %} 39 |
Age{{ blk.timestamp | date:"Y-m-d, H:i:s" }} UTC
PoW Algorithm 48 | {% if blk.edge_bits == 29 %} 49 | cuckARoo-29 50 | {% else %} 51 | cuckAToo-{{ blk.edge_bits }} 52 | {% endif %} 53 |
Secondary Scale{{ blk.secondary_scaling | intcomma }}
Solution Difficulty{{ blk.difficulty | floatformat:-2 | intcomma }}
Target Difficulty{{ blk.target_difficulty | floatformat:-2 | intcomma }}
Total Difficulty{{ blk.total_difficulty | intcomma }}
Total Kernel Offset{{ blk.total_kernel_offset }}
Output MMR Size{{ blk.output_mmr_size }}
Kernel MMR Size{{ blk.kernel_mmr_size }}
Nonce{{ blk.nonce }}
Block Reward{{ blk.reward | grin }}
Fees{{ blk.fees | nanogrin }}
100 |
101 |
102 |
103 | 104 |
105 |
106 |
107 | Inputs ({{ blk.input_set.all.count }}) 108 |
109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | {% for input in blk.input_set.all %} 117 | 118 | 120 | 121 | {% endfor %} 122 | 123 |
Commit
{{ input.data }}
124 |
125 |
126 |
127 |
128 | 129 |
130 | 131 |
132 |
133 |
134 | Outputs ({{ blk.output_set.all.count }}) 135 |
136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | {% for output in blk.output_set.all %} 145 | 146 | 147 | 150 | 151 | {% endfor %} 152 | 153 |
Output TypeCommit
{{ output.output_type }}{{ output.commit }} 149 |
154 |
155 |
156 |
157 |
158 | 159 |
160 |
161 |
162 |
163 | Kernels ({{ blk.kernel_set.all.count }}) 164 |
165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | {% for kernel in blk.kernel_set.all %} 175 | 176 | 177 | 178 | 179 | 180 | {% endfor %} 181 | 182 |
FeaturesFeeLock Height
{{ kernel.features }}{{ kernel.fee | nanogrin }}{{ kernel.lock_height }}
183 |
184 |
185 |
186 |
187 | 188 | {% endblock content %} 189 | -------------------------------------------------------------------------------- /grinexplorer/explorer/templates/explorer/block_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load static %} 4 | {% load humanize %} 5 | {% load grin %} 6 | {% load shortnaturaltime %} 7 | 8 | {% block chart_loader_header %} 9 | {% if block_list.exists %} 10 | 11 | 12 | 13 | 14 | 15 | {% load chartit %} 16 | {{ thumb_chart_list|load_charts:"thumbnail_blockchart, thumbnail_feechart" }} 17 | {% endif %} 18 | {% endblock %} 19 | 20 | {% block content %} 21 | {% if block_list.exists %} 22 |
23 | 24 | 44 | 45 |
46 | 47 |
48 |
49 |
50 |
51 | Chain Height 52 |
53 |
54 |

{{ highest_block.height | intcomma }}

55 |
56 |
57 |
58 | 59 |
60 |
61 |
62 | Latest Block 63 |
64 |
65 |

{{ latest_block.timestamp | naturaltime }}

66 |
67 |
68 |
69 | 70 |
71 |
72 |
73 | Latest Block Difficulty 74 |
75 |
76 |

77 | {{ latest_block.difficulty | intcomma }} / {{ latest_block.target_difficulty | intcomma }} 78 |

79 |
80 |
81 |
82 | 83 |
84 |
85 |
86 | Emission 87 |
88 |
89 |

{{ total_emission | grin }}

90 |
91 |
92 |
93 |
94 | 95 |
96 | 97 | {% if competing_chains > 1 %} 98 | 103 | {% endif %} 104 | 105 | {% if block_list %} 106 |
107 |
108 |

109 |   Blocks

110 |
111 |
112 | {% if is_paginated %} 113 | 128 | {% endif %} 129 | 130 | {% else %} 131 | Sorry, there was an error fetching the list of blocks. Please try again later. 132 | {% endif %} 133 |
134 |
135 | 136 |
137 |
138 |
139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | {% for blk in block_list %} 154 | 155 | 156 | 159 | 160 | 161 | 168 | 169 | 170 | 171 | 172 | {% endfor %} 173 | 174 |
HeightHashAgeDifficultyPoW Algo# Kernels# Inputs# Outputs
{{ blk.height }} 157 | {{ blk.hash }} 158 | {{ blk.timestamp | shortnaturaltime }}{{ blk.target_difficulty | intcomma }} 162 | {% if blk.edge_bits == 29 %} 163 | AR-29 164 | {% else %} 165 | AT-{{ blk.edge_bits }} 166 | {% endif %} 167 | {{ blk.kernel_set.all.count }}{{ blk.input_set.all.count }}{{ blk.output_set.all.count }}
175 | {% else %} 176 |

177 |

Block Database is empty

178 |

Please run ./manage.py import_from_tip ... to populate the database.

179 | {% endif %} 180 | 181 |
182 |
183 |
184 | {% endblock %} 185 | -------------------------------------------------------------------------------- /grinexplorer/explorer/templates/explorer/blocks_by_height.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load humanize %} 4 | 5 | {% block content %} 6 |
7 |
8 | 9 |

Block {{ height }}

10 | 11 |

12 | There was a fork in the blockchain resulting in several competing blocks at this height: 13 |

14 | 15 |
16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {% for blk in blocks %} 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | {% endfor %} 40 | 41 |
HashAgeCumulative Difficulty# kernel# in# out
{{ blk.hash }}{{ blk.timestamp | naturaltime}}{{ blk.total_difficulty | intcomma }}{{ blk.kernel_set.all.count }}{{ blk.input_set.all.count }}{{ blk.output_set.all.count }}
42 |
43 |
44 |
45 | {% endblock content %} -------------------------------------------------------------------------------- /grinexplorer/explorer/templates/explorer/fee_chart.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block chart_loader_header %} 5 | 6 | 7 | 8 | 9 | 10 | {% load chartit %} 11 | {{ feechart|load_charts:"containerfeechart" }} 12 | {% endblock %} 13 | 14 | 15 | {% block content %} 16 |
17 | 18 |
19 |
20 |
21 |
22 |
23 |
24 | 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /grinexplorer/explorer/templates/explorer/output_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load static %} 4 | {% load humanize %} 5 | {% load grin %} 6 | 7 | {% block content %} 8 |
9 |
10 | 11 |
12 |
13 |

14 |   Output {{ output.commit|slice:"10" }}...

15 |
16 |
17 | 18 |
19 |
20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | 42 | 43 | 44 | {% if output.spent %} 45 | 46 | 47 | 49 | 50 | {% endif %} 51 |
Output Type{{ output.output_type }}
Commit{{ output.commit }}
Occurrences{{ output.occurrences }}
Block Height{{ output.block_height }} 38 |
Spent{{ output.spent }}
Spent at Height{{ output.spent_at }} 48 |
52 |
53 |
54 |
55 | 56 | {% endblock content %} -------------------------------------------------------------------------------- /grinexplorer/explorer/templates/explorer/search_results.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load humanize %} 4 | 5 | {% block content %} 6 |
7 |
8 | 9 |

Search Results for "{{ q }}"

10 | 11 | {% if results %} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% for blk in results %} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | {% endfor %} 34 | 35 |
HashAgeCumulative Difficulty# Kernels# Inputs# Outputs
{{ blk.hash }}{{ blk.timestamp | naturaltime}}{{ blk.total_difficulty | intcomma }}{{ blk.kernel_set.all.count }}{{ blk.input_set.all.count }}{{ blk.output_set.all.count }}
36 | {% else %} 37 | {% if q_isdigit %} 38 |

39 | We couldn't find a block at height {{ q }}. 40 |

41 | {% elif q|length < 6 %} 42 |

43 | Your search term {{ q }} is too short, please enter at least six characters if searching by hash. 44 |

45 | {% else %} 46 |

47 | We couldn't find any blocks or outputs starting with the hash {{ q }}. 48 |

49 | {% endif %} 50 | {% endif %} 51 | {% endblock content %} -------------------------------------------------------------------------------- /grinexplorer/explorer/templatetags/grin.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | from django import template 3 | 4 | register = template.Library() 5 | 6 | 7 | def format_float(f): 8 | s = "{:,f}".format(f) 9 | if "." not in s: 10 | return s 11 | 12 | return s.rstrip("0").rstrip(".") 13 | 14 | 15 | @register.filter 16 | def nanogrin(nanogrin): 17 | if nanogrin == 0: 18 | return grin(0) 19 | 20 | if nanogrin < 1000: 21 | return "%d ng" % nanogrin 22 | 23 | return microgrin(Decimal(nanogrin) / Decimal(1000)) 24 | 25 | 26 | @register.filter 27 | def microgrin(microgrin): 28 | if microgrin == 0: 29 | return grin(0) 30 | 31 | if microgrin < 1000: 32 | return "%s µg" % format_float(microgrin) 33 | 34 | return milligrin(Decimal(microgrin) / Decimal(1000)) 35 | 36 | 37 | @register.filter 38 | def milligrin(milligrin): 39 | if milligrin == 0: 40 | return grin(0) 41 | 42 | if milligrin < 1000: 43 | return "%s mg" % format_float(milligrin) 44 | 45 | return grin(Decimal(milligrin) / Decimal(1000)) 46 | 47 | 48 | @register.filter 49 | def grin(grin): 50 | return "%s grin" % format_float(grin) 51 | -------------------------------------------------------------------------------- /grinexplorer/explorer/templatetags/shortnaturaltime.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.utils.timezone import utc 3 | from datetime import datetime, timedelta 4 | 5 | register = template.Library() 6 | 7 | 8 | def _now(): 9 | return datetime.utcnow().replace(tzinfo=utc) 10 | 11 | 12 | def abs_timedelta(delta): 13 | """Returns an "absolute" value for a timedelta, always representing a 14 | time distance.""" 15 | if delta.days < 0: 16 | now = _now() 17 | return now - (now + delta) 18 | return delta 19 | 20 | 21 | def date_and_delta(value): 22 | """Turn a value into a date and a timedelta which represents how long ago 23 | it was. If that's not possible, return (None, value).""" 24 | now = _now() 25 | if isinstance(value, datetime): 26 | date = value 27 | delta = now - value 28 | elif isinstance(value, timedelta): 29 | date = now - value 30 | delta = value 31 | else: 32 | try: 33 | value = int(value) 34 | delta = timedelta(seconds=value) 35 | date = now - delta 36 | except (ValueError, TypeError): 37 | return None, value 38 | return date, abs_timedelta(delta) 39 | 40 | 41 | def shortnaturaldelta(value): 42 | """Given a timedelta or a number of seconds, return a natural 43 | representation of the amount of time elapsed.""" 44 | date, delta = date_and_delta(value) 45 | if date is None: 46 | return value 47 | 48 | seconds = abs(delta.seconds) 49 | days = abs(delta.days) 50 | years = days // 365 51 | days = days % 365 52 | 53 | if not years and days < 1: 54 | if seconds <= 1: 55 | return "1s" 56 | elif seconds < 60: 57 | return "%ds" % (seconds) 58 | elif 60 <= seconds < 3600: 59 | return "%dm %ds" % (seconds // 60, seconds % 60) 60 | elif 3600 <= seconds: 61 | return "%dh %dm %ds" % (seconds // 3600, seconds % 3600 // 60, seconds % 60) 62 | elif years == 0: 63 | return "%dd %dh %dm" % (days, seconds % 86400 // 3600, seconds % 3600 // 60) 64 | else: 65 | return "%dy %dd %dh" % (years, days, seconds % 86400 // 3600) 66 | 67 | 68 | @register.filter 69 | def shortnaturaltime(value): 70 | """Given a datetime or a number of seconds, return a natural representation 71 | of that time in a resolution that makes sense. This is more or less 72 | compatible with Django's ``naturaltime`` filter.""" 73 | # now = _now() 74 | date, delta = date_and_delta(value) 75 | if date is None: 76 | return value 77 | # determine tense by value only if datetime/timedelta were passed 78 | # if isinstance(value, (datetime, timedelta)): 79 | # future = date > now 80 | 81 | delta = shortnaturaldelta(delta) 82 | 83 | if delta == "a moment": 84 | return "now" 85 | 86 | return delta 87 | -------------------------------------------------------------------------------- /grinexplorer/explorer/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /grinexplorer/explorer/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import BlockList, BlocksByHeight, BlockDetail, OutputByCommit, Search 4 | from .charts import block_chart, fee_chart 5 | 6 | 7 | urlpatterns = [ 8 | path("", BlockList.as_view(), name="block-list"), 9 | path("chart/block", block_chart, name="block-chart"), 10 | path("chart/fee", fee_chart, name="fee-chart"), 11 | path("block/", BlocksByHeight.as_view(), name="blocks-by-height"), 12 | path("block/", BlockDetail.as_view(), name="block-detail"), 13 | path("output/", OutputByCommit.as_view(), name="output-detail"), 14 | path("search", Search.as_view(), name="search"), 15 | ] 16 | -------------------------------------------------------------------------------- /grinexplorer/explorer/views.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Count, Sum, Max, Min 2 | from django.db.models.functions import TruncDay 3 | from django.views.generic import ListView, DetailView, TemplateView 4 | from django.urls import reverse 5 | from django.shortcuts import redirect 6 | 7 | from blockchain.models import Block, Input, Output 8 | from chartit import DataPool, Chart 9 | 10 | 11 | class BlockList(ListView): 12 | template_name = "explorer/block_list.html" 13 | context_object_name = "block_list" 14 | 15 | queryset = Block.objects.order_by("-timestamp") \ 16 | .select_related("previous") \ 17 | .prefetch_related("output_set", "kernel_set", "input_set") 18 | paginate_by = 20 19 | 20 | def get_block_chart(self): 21 | blockpivotdata = DataPool( 22 | series=[{ 23 | 'options': { 24 | 'source': Block.objects.raw("select 1 as hash, to_char(timestamp,'MM-dd') as niceday, " 25 | "max(total_difficulty) as total_difficulty, " 26 | "date(DATE_TRUNC('day', timestamp)) as date, count(hash) as num " 27 | "from blockchain_block where timestamp > current_date - interval '30 day'" 28 | "group by DATE_TRUNC('day', timestamp),niceday order by date") 29 | }, 30 | 'terms': [ 31 | 'niceday', 32 | 'num', 33 | 'total_difficulty', 34 | ] 35 | }] 36 | ) 37 | 38 | blockpivcht = Chart( 39 | datasource=blockpivotdata, 40 | series_options=[{ 41 | 'options': { 42 | 'type': 'line', 43 | 'xAxis': 0, 44 | 'yAxis': 0, 45 | 'zIndex': 1, 46 | 'legendIndex': 1, 47 | }, 48 | 'terms': { 49 | 'niceday': ['num'] 50 | }}, { 51 | 'options': { 52 | 'type': 'line', 53 | 'xAxis': 1, 54 | 'yAxis': 1, 55 | 'legendIndex': 0, 56 | }, 57 | 'terms': { 58 | 'niceday': ['total_difficulty'] 59 | } 60 | }], 61 | chart_options={ 62 | 'title': { 63 | 'text': 'Blocks and Total Difficulty' 64 | }, 65 | 'xAxis': [ 66 | { 67 | 'title': { 68 | 'text': 'Date', 69 | 'style': { 70 | 'display': 'none' 71 | } 72 | }, 73 | 'labels': { 74 | 'enabled': True 75 | } 76 | }, 77 | { 78 | 'title': { 79 | 'text': 'Date', 80 | 'style': { 81 | 'display': 'none' 82 | } 83 | }, 84 | 'labels': { 85 | 'enabled': False 86 | }, 87 | 'lineColor': 'transparent', 88 | 'tickLength': 0, 89 | } 90 | ], 91 | 'yAxis': [ 92 | { 93 | 'title': { 94 | 'text': 'Blocks', 95 | 'style': { 96 | 'color': '#70b3ef' 97 | } 98 | }, 99 | 'labels': { 100 | 'enabled': False 101 | }, 102 | }, 103 | { 104 | 'title': { 105 | 'text': 'Total Diff' 106 | }, 107 | 'labels': { 108 | 'enabled': False 109 | } 110 | } 111 | ], 112 | 'legend': { 113 | 'enabled': False 114 | }, 115 | } 116 | ) 117 | return blockpivcht 118 | 119 | def get_fee_chart(self): 120 | feepivotdata = DataPool( 121 | series=[{ 122 | 'options': { 123 | 'source': Block.objects.raw("select 1 as hash, to_char(timestamp,'MM-dd') as niceday, " 124 | "date(DATE_TRUNC('day', timestamp)) as date, sum(fee)/1000000 as fee " 125 | "from blockchain_block t1 join blockchain_kernel t2 " 126 | "on t2.block_id=t1.hash where timestamp > current_date - interval '30 day' " 127 | "group by DATE_TRUNC('day', timestamp),niceday order by date") 128 | }, 129 | 'terms': [ 130 | 'niceday', 131 | 'fee' 132 | ] 133 | }] 134 | ) 135 | 136 | feepivcht = Chart( 137 | datasource=feepivotdata, 138 | series_options=[{ 139 | 'options': { 140 | 'type': 'line', 141 | 'stacking': False 142 | }, 143 | 'terms': { 144 | 'niceday': [ 145 | 'fee', 146 | ] 147 | } 148 | }], 149 | chart_options={ 150 | 'title': { 151 | 'text': 'Transaction Fee Chart'}, 152 | 'xAxis': { 153 | 'title': { 154 | 'text': 'Date'}}, 155 | 'yAxis': { 156 | 'title': { 157 | 'text': 'Tx Fee'}}, 158 | 'legend': { 159 | 'enabled': False}, 160 | 'credits': { 161 | 'enabled': False}}, 162 | ) 163 | return feepivcht 164 | 165 | def get_context_data(self, **kwargs): 166 | context = super().get_context_data(**kwargs) 167 | 168 | if Block.objects.exists(): 169 | context["highest_block"] = Block.objects.order_by("height").last() 170 | context["latest_block"] = Block.objects.order_by( 171 | "timestamp").last() 172 | context["total_emission"] = Block.objects.order_by( 173 | "total_difficulty").last().height * 60 174 | 175 | context["competing_chains"] = Block.objects \ 176 | .filter(height__gte=context["highest_block"].height - 60) \ 177 | .values("height") \ 178 | .annotate(cnt=Count("height")) \ 179 | .aggregate(Max("cnt"))["cnt__max"] 180 | context["forked_at"] = Block.objects \ 181 | .filter(height__gte=context["highest_block"].height - 60) \ 182 | .values("height") \ 183 | .annotate(cnt=Count("height")) \ 184 | .filter(cnt__gt=1) \ 185 | .aggregate(Min("height"))["height__min"] 186 | 187 | context['thumb_chart_list'] = [ 188 | self.get_block_chart(), self.get_fee_chart()] 189 | 190 | return context 191 | 192 | 193 | class BlockDetail(DetailView): 194 | model = Block 195 | 196 | template_name = "explorer/block_detail.html" 197 | context_object_name = "blk" 198 | queryset = Block.objects.select_related("previous") \ 199 | .prefetch_related("output_set", "kernel_set", "input_set") 200 | 201 | 202 | class OutputByCommit(TemplateView): 203 | template_name = "explorer/output_detail.html" 204 | 205 | def get_context_data(self, **kwargs): 206 | context = super().get_context_data(**kwargs) 207 | 208 | context["output"] = self.output 209 | 210 | return context 211 | 212 | def get(self, request, commit): 213 | outputs = Output.objects.filter(commit=commit) \ 214 | .order_by("-id") 215 | 216 | if len(outputs) == 0: 217 | return redirect("%s?q=%s" % (reverse("search"), commit), 218 | permanent=False) 219 | 220 | self.output = outputs[0] 221 | self.output.occurrences = len(outputs) 222 | temp_input = Input.objects.filter(data=commit) 223 | if len(temp_input) != 0: 224 | # update as spent 225 | self.output.spent = True 226 | self.output.spent_at = temp_input[0].block.height 227 | return super().get(request) 228 | 229 | 230 | class BlocksByHeight(TemplateView): 231 | template_name = "explorer/blocks_by_height.html" 232 | 233 | def get_context_data(self, **kwargs): 234 | context = super().get_context_data(**kwargs) 235 | 236 | context["blocks"] = self.blocks 237 | context["height"] = self.height 238 | 239 | return context 240 | 241 | def get(self, request, height): 242 | self.blocks = Block.objects.filter( 243 | height=height).order_by("-total_difficulty") 244 | self.height = height 245 | 246 | if len(self.blocks) == 1: 247 | return redirect("block-detail", pk=self.blocks[0].hash, permanent=False) 248 | else: 249 | return super().get(request) 250 | 251 | 252 | class Search(TemplateView): 253 | template_name = "explorer/search_results.html" 254 | results = None 255 | q_isdigit = False 256 | 257 | def get_context_data(self, **kwargs): 258 | context = super().get_context_data(**kwargs) 259 | 260 | context["q"] = self.q 261 | context["q_isdigit"] = self.q_isdigit 262 | context["results"] = self.results 263 | 264 | return context 265 | 266 | def get(self, request): 267 | self.q = request.GET.get("q", "").strip() 268 | 269 | # search query is valid block height 270 | if self.q.isdigit(): 271 | self.q_isdigit = True 272 | 273 | if Block.objects.filter(height=self.q).count(): 274 | return redirect("blocks-by-height", height=self.q, permanent=False) 275 | 276 | # commitment are 66 characters long 277 | if len(self.q) == 66: 278 | self.results = Output.objects.filter( 279 | commit=self.q).order_by("-id") 280 | 281 | # if only one result, redirect to commit detail 282 | if self.results.count() != 0: 283 | return redirect("output-detail", commit=self.results[0].commit, permanent=False) 284 | 285 | if len(self.q) > 6: 286 | self.results = Block.objects.filter(hash__startswith=self.q) 287 | 288 | # if only one result, redirect to found block 289 | if self.results.count() == 1: 290 | return redirect("block-detail", pk=self.results[0].hash, permanent=False) 291 | 292 | return super().get(request) 293 | -------------------------------------------------------------------------------- /grinexplorer/grinexplorer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-explorer/59b66966561e6e522807dc272a96ea72f69d9524/grinexplorer/grinexplorer/__init__.py -------------------------------------------------------------------------------- /grinexplorer/grinexplorer/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for grinexplorer project. 3 | 4 | Generated by "django-admin startproject" using Django 2.0. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | from django.core.exceptions import ImproperlyConfigured 15 | 16 | 17 | def env(setting): 18 | try: 19 | return os.environ[setting] 20 | except KeyError: 21 | error_msg = "Please set the '%s' env variable" % setting 22 | raise ImproperlyConfigured(error_msg) 23 | 24 | 25 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 26 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 27 | 28 | # Quick-start development settings - unsuitable for production 29 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ 30 | 31 | # SECURITY WARNING: keep the secret key used in production secret! 32 | SECRET_KEY = env("SECRET_KEY") 33 | 34 | # SECURITY WARNING: don"t run with debug turned on in production! 35 | DEBUG = False 36 | 37 | ALLOWED_HOSTS = ["127.0.0.1", "localhost", "grinexplorer.net"] 38 | 39 | 40 | # Application definition 41 | 42 | INSTALLED_APPS = [ 43 | "django.contrib.admin", 44 | "django.contrib.auth", 45 | "django.contrib.contenttypes", 46 | "django.contrib.sessions", 47 | "django.contrib.messages", 48 | "django.contrib.staticfiles", 49 | "django.contrib.humanize", 50 | 51 | "chartit", 52 | 53 | "blockchain", 54 | "explorer", 55 | ] 56 | 57 | MIDDLEWARE = [ 58 | "django.middleware.security.SecurityMiddleware", 59 | "django.contrib.sessions.middleware.SessionMiddleware", 60 | "django.middleware.common.CommonMiddleware", 61 | "django.middleware.csrf.CsrfViewMiddleware", 62 | "django.contrib.auth.middleware.AuthenticationMiddleware", 63 | "django.contrib.messages.middleware.MessageMiddleware", 64 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 65 | ] 66 | 67 | ROOT_URLCONF = "grinexplorer.urls" 68 | 69 | TEMPLATES = [ 70 | { 71 | "BACKEND": "django.template.backends.django.DjangoTemplates", 72 | "DIRS": ["templates"], 73 | "APP_DIRS": True, 74 | "OPTIONS": { 75 | "context_processors": [ 76 | "django.template.context_processors.debug", 77 | "django.template.context_processors.request", 78 | "django.contrib.auth.context_processors.auth", 79 | "django.contrib.messages.context_processors.messages", 80 | ], 81 | }, 82 | }, 83 | ] 84 | 85 | WSGI_APPLICATION = "grinexplorer.wsgi.application" 86 | 87 | 88 | # Database 89 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases 90 | 91 | DATABASES = { 92 | "default": { 93 | "ENGINE": "django.db.backends.postgresql", 94 | "NAME": env("DB_NAME"), 95 | "USER": env("DB_USER"), 96 | "PASSWORD": env("DB_PASSWORD"), 97 | "HOST": env("DB_HOST"), 98 | "PORT": env("DB_PORT"), 99 | } 100 | } 101 | 102 | 103 | # Password validation 104 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators 105 | 106 | AUTH_PASSWORD_VALIDATORS = [ 107 | { 108 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 109 | }, 110 | { 111 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 112 | }, 113 | { 114 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 115 | }, 116 | { 117 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 118 | }, 119 | ] 120 | 121 | 122 | # Internationalization 123 | # https://docs.djangoproject.com/en/2.0/topics/i18n/ 124 | 125 | LANGUAGE_CODE = "en-us" 126 | 127 | TIME_ZONE = "UTC" 128 | 129 | USE_I18N = True 130 | 131 | USE_L10N = True 132 | 133 | USE_TZ = True 134 | 135 | 136 | # Static files (CSS, JavaScript, Images) 137 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 138 | 139 | STATIC_URL = "/static/" 140 | 141 | STATICFILES_FINDERS = ( 142 | 'django.contrib.staticfiles.finders.FileSystemFinder', 143 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 144 | ) 145 | 146 | STATIC_ROOT = os.path.join(os.path.dirname(BASE_DIR), 'static') 147 | STATICFILES_DIRS = [ 148 | os.path.join(BASE_DIR, "static"), 149 | ] 150 | -------------------------------------------------------------------------------- /grinexplorer/grinexplorer/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | 3 | 4 | urlpatterns = [ 5 | path("", include("explorer.urls")), 6 | ] 7 | -------------------------------------------------------------------------------- /grinexplorer/grinexplorer/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for grinexplorer project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "grinexplorer.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /grinexplorer/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "grinexplorer.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /grinexplorer/static/css/project.css: -------------------------------------------------------------------------------- 1 | html { 2 | position:relative; 3 | min-height:100%; 4 | 5 | } 6 | 7 | body { 8 | margin:10px 0px 40px 0px; 9 | } 10 | 11 | footer { 12 | position:absolute; 13 | bottom:0; 14 | width:100%; 15 | height:24px; 16 | line-height:24px; 17 | background-color:#212529; 18 | color:#ccc; 19 | font-size:12px; 20 | text-align:center; 21 | } 22 | 23 | details.black summary { 24 | background-color:#999999; 25 | color:#fff; 26 | font-weight:bold; 27 | line-height:40px; 28 | padding-left:8px; 29 | } 30 | 31 | td.numeric { 32 | text-align: left; 33 | } 34 | 35 | tr.text-muted > td > a { 36 | color:#646c74!important; 37 | } 38 | 39 | -------------------------------------------------------------------------------- /grinexplorer/static/css/style.css: -------------------------------------------------------------------------------- 1 | { 2 | % load static% 3 | } 4 | 5 | html { 6 | font-size: 13px; 7 | } 8 | 9 | body { 10 | margin: 0; 11 | font-family: Roboto, sans-serif, monospace; 12 | line-height: 1.5; 13 | color: #707070; 14 | background-color: #F3F3F3; 15 | } 16 | 17 | main { 18 | position: relative; 19 | } 20 | 21 | .monospace { 22 | font-family: Courier; 23 | } 24 | 25 | .header { 26 | background-color: #3F51B5; 27 | position: fixed; 28 | width: 100%; 29 | height: 6rem; 30 | box-shadow: 0 1px 10px rgba(0, 0, 0, .2); 31 | padding: 0; 32 | display: flex; 33 | align-items: center; 34 | color: #FFFFFF; 35 | z-index: 10; 36 | } 37 | 38 | .header_container { 39 | padding: 0 0 0 2rem; 40 | width: 100%; 41 | height: 100%; 42 | display: flex; 43 | align-items: center; 44 | } 45 | 46 | li.page-item { 47 | min-width: 6em; 48 | } 49 | 50 | 51 | .header_info { 52 | align-items: center; 53 | height: 100%; 54 | vertical-align: middle; 55 | display: flex; 56 | padding-right: 0; 57 | } 58 | 59 | .header_info div { 60 | display: block; 61 | float: right; 62 | } 63 | 64 | /*.header_info div { 65 | float: left; 66 | }*/ 67 | 68 | .header_height { 69 | margin-left: auto; 70 | } 71 | 72 | .header_height span, 73 | .header_hash>span { 74 | display: block; 75 | } 76 | 77 | .header_hash, 78 | .header_status { 79 | padding-left: 1em; 80 | } 81 | 82 | /*.header_block #latest_block_diff { 83 | display: inline; 84 | padding-left: 5px; 85 | }*/ 86 | 87 | /*.header_first_block span { 88 | text-align: right; 89 | }*/ 90 | 91 | .header h1 { 92 | line-height: 100%; 93 | font-size: 1.5rem; 94 | font-weight: normal; 95 | margin: 0; 96 | display: inline; 97 | } 98 | 99 | .header h4 { 100 | line-height: 100%; 101 | font-size: 1.1rem; 102 | font-weight: normal; 103 | margin: 0; 104 | display: inline; 105 | } 106 | 107 | /*#status { 108 | text-align: center; 109 | width: 30px; 110 | }*/ 111 | 112 | #status .fa-times { 113 | color: red; 114 | } 115 | 116 | #status .fa-check { 117 | color: lightgreen; 118 | } 119 | 120 | #latest_block_height { 121 | font-size: 2rem; 122 | text-align: right; 123 | } 124 | 125 | #latest_block_time { 126 | font-size: 1.1rem; 127 | text-align: right; 128 | } 129 | 130 | 131 | .sidebar { 132 | width: 15rem; 133 | position: fixed; 134 | left: 0; 135 | padding: 7rem 2rem .5rem; 136 | height: 100%; 137 | overflow: hidden; 138 | } 139 | 140 | .nav { 141 | display: list-item; 142 | list-style: none; 143 | padding: 0; 144 | font-size: 1.2rem; 145 | } 146 | 147 | .nav a { 148 | padding: .85rem .5rem; 149 | display: block; 150 | text-decoration: none; 151 | color: #707070; 152 | } 153 | 154 | .nav a:hover { 155 | background-color: #E5E5E5; 156 | } 157 | 158 | .nav .nav_active a { 159 | color: #3F51B5; 160 | font-weight: bold; 161 | } 162 | 163 | .nav i { 164 | vertical-align: top; 165 | font-size: 1rem; 166 | position: relative; 167 | top: .2rem; 168 | width: 1.5rem; 169 | text-align: center; 170 | margin-right: .6rem; 171 | } 172 | 173 | .content { 174 | padding: 8rem 2rem 2rem 15.5rem; 175 | } 176 | 177 | .content_title { 178 | margin-left: -1.85rem; 179 | } 180 | 181 | .b-a-2 { 182 | border-width: 2px; 183 | } 184 | 185 | .panel-primary { 186 | border-color: #2c97de; 187 | } 188 | 189 | .panel-info { 190 | border-color: #2dbda8; 191 | } 192 | 193 | .panel-warning { 194 | border-color: #e76d3b; 195 | } 196 | 197 | tr.clickable:hover { 198 | cursor: pointer; 199 | background-color: #e3e3e3 !important; 200 | } 201 | 202 | #max_block_count_container { 203 | float: right; 204 | } 205 | 206 | .autofit { 207 | display: table; 208 | table-layout: fixed; 209 | width: 100%; 210 | } 211 | 212 | .autofit div { 213 | overflow-x: hidden; 214 | text-overflow: ellipsis; 215 | white-space: nowrap; 216 | } 217 | 218 | .table-autofit td { 219 | white-space: nowrap; 220 | } 221 | 222 | .alert span { 223 | font-weight: bold; 224 | } 225 | 226 | .table-nonfluid { 227 | width: auto !important; 228 | } 229 | 230 | .table-nonfluid td { 231 | vertical-align: middle; 232 | } 233 | 234 | .table-horizontal-bordered td { 235 | border-bottom: 1px solid #ddd; 236 | } 237 | 238 | #custom-search-input { 239 | padding: 3px; 240 | border: solid 1px #E4E4E4; 241 | border-radius: 0.4rem; 242 | background-color: #fff; 243 | } 244 | 245 | #custom-search-input input { 246 | border: 0; 247 | box-shadow: none; 248 | } 249 | 250 | #custom-search-input button { 251 | margin: 2px 0 0 0; 252 | background: none; 253 | box-shadow: none; 254 | border: 0; 255 | color: #666666; 256 | padding: 0 0.5rem 0 0.6rem; 257 | border-left: solid 1px #ccc; 258 | } 259 | 260 | .fork_warning { 261 | margin-left: 2em; 262 | } 263 | 264 | .network_selector { 265 | width: 100%; 266 | text-align: center; 267 | padding-top: 2rem; 268 | } 269 | 270 | .network_selector select { 271 | padding: 0.5rem; 272 | border-radius: 0.4rem; 273 | } 274 | 275 | .network_selector span { 276 | display: block; 277 | font-weight: bold; 278 | } 279 | 280 | #latest_block_direction { 281 | padding-right: 0.4rem; 282 | } 283 | 284 | .graph_container { 285 | position: relative; 286 | } 287 | 288 | .graph_hover { 289 | font-weight: bold; 290 | position: absolute; 291 | top: 17%; 292 | right: 11%; 293 | } 294 | 295 | .graph_hover_legend { 296 | font-weight: bold; 297 | position: absolute; 298 | top: 17%; 299 | right: 16.5%; 300 | } -------------------------------------------------------------------------------- /grinexplorer/static/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-explorer/59b66966561e6e522807dc272a96ea72f69d9524/grinexplorer/static/images/favicon-32x32.png -------------------------------------------------------------------------------- /grinexplorer/static/images/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-explorer/59b66966561e6e522807dc272a96ea72f69d9524/grinexplorer/static/images/favicon-96x96.png -------------------------------------------------------------------------------- /grinexplorer/static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-explorer/59b66966561e6e522807dc272a96ea72f69d9524/grinexplorer/static/images/favicon.ico -------------------------------------------------------------------------------- /grinexplorer/static/images/icons/logo_80x80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-explorer/59b66966561e6e522807dc272a96ea72f69d9524/grinexplorer/static/images/icons/logo_80x80.png -------------------------------------------------------------------------------- /grinexplorer/static/webfonts/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Font Awesome Free License 2 | ------------------------- 3 | 4 | Font Awesome Free is free, open source, and GPL friendly. You can use it for 5 | commercial projects, open source projects, or really almost whatever you want. 6 | Full Font Awesome Free license: https://fontawesome.com/license. 7 | 8 | # Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/) 9 | In the Font Awesome Free download, the CC BY 4.0 license applies to all icons 10 | packaged as SVG and JS file types. 11 | 12 | # Fonts: SIL OFL 1.1 License (https://scripts.sil.org/OFL) 13 | In the Font Awesome Free download, the SIL OLF license applies to all icons 14 | packaged as web and desktop font files. 15 | 16 | # Code: MIT License (https://opensource.org/licenses/MIT) 17 | In the Font Awesome Free download, the MIT license applies to all non-font and 18 | non-icon files. 19 | 20 | # Attribution 21 | Attribution is required by MIT, SIL OLF, and CC BY licenses. Downloaded Font 22 | Awesome Free files already contain embedded comments with sufficient 23 | attribution, so you shouldn't need to do anything additional when using these 24 | files normally. 25 | 26 | We've kept attribution comments terse, so we ask that you do not actively work 27 | to remove them from files, especially code. They're a great way for folks to 28 | learn about Font Awesome. 29 | 30 | # Brand Icons 31 | All brand icons are trademarks of their respective owners. The use of these 32 | trademarks does not indicate endorsement of the trademark holder by Font 33 | Awesome, nor vice versa. **Please do not use brand logos for any purpose except 34 | to represent the company, product, or service to which they refer.** 35 | -------------------------------------------------------------------------------- /grinexplorer/static/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-explorer/59b66966561e6e522807dc272a96ea72f69d9524/grinexplorer/static/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /grinexplorer/static/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-explorer/59b66966561e6e522807dc272a96ea72f69d9524/grinexplorer/static/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /grinexplorer/static/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-explorer/59b66966561e6e522807dc272a96ea72f69d9524/grinexplorer/static/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /grinexplorer/static/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-explorer/59b66966561e6e522807dc272a96ea72f69d9524/grinexplorer/static/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /grinexplorer/static/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-explorer/59b66966561e6e522807dc272a96ea72f69d9524/grinexplorer/static/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /grinexplorer/static/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-explorer/59b66966561e6e522807dc272a96ea72f69d9524/grinexplorer/static/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /grinexplorer/static/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-explorer/59b66966561e6e522807dc272a96ea72f69d9524/grinexplorer/static/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /grinexplorer/static/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-explorer/59b66966561e6e522807dc272a96ea72f69d9524/grinexplorer/static/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /grinexplorer/static/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-explorer/59b66966561e6e522807dc272a96ea72f69d9524/grinexplorer/static/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /grinexplorer/static/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-explorer/59b66966561e6e522807dc272a96ea72f69d9524/grinexplorer/static/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /grinexplorer/static/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-explorer/59b66966561e6e522807dc272a96ea72f69d9524/grinexplorer/static/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /grinexplorer/static/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimblewimble/grin-explorer/59b66966561e6e522807dc272a96ea72f69d9524/grinexplorer/static/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /grinexplorer/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | {% load static %} 3 | 4 | 5 | 6 | 7 | GrinExplorer 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | {% block chart_loader_header %}{% endblock %} 19 | 20 | 21 | 22 | 23 |
24 | 44 | 45 | {% block content %}{% endblock %} 46 |
47 | 48 |
49 |
50 | 51 | 55 | 58 | 61 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==2.2.24 2 | psycopg2==2.8.6 3 | requests==2.20.0 4 | django_chartit==0.2.9 5 | --------------------------------------------------------------------------------