Compare commits

...

344 Commits

Author SHA1 Message Date
natsoni
af7e5fa498 Liquid audit re-indexing 2024-04-18 12:13:57 +02:00
natsoni
ac62e28656 Merge branch 'natsoni/fix-liquid-db-migration' into natsoni/fix-liquid-expired-txos 2024-04-18 11:56:27 +02:00
natsoni
1c55eef276 Move table re-index to own migrations 2024-04-16 22:43:13 +02:00
natsoni
13aa62878e Revert commits abdb27af and 727208ff 2024-04-16 16:12:31 +02:00
natsoni
2c676ae06e Fix gradient colors in Liquid federation utxos table 2024-04-16 12:15:45 +02:00
natsoni
f897679ed6 Fix Liquid expired utxos flagging mechanism 2024-04-16 12:15:14 +02:00
wiz
7f8dc74146 Merge pull request #4975 from mempool/nymkappa/mini-branch
[accelerator] polish pizza accel
2024-04-16 17:11:55 +09:00
wiz
83cea33727 Merge pull request #4969 from mempool/nymkappa/missing-accelerate-button-mobile
[accelerator] re-add missing accelerator button on mobile, set locati…
2024-04-16 17:02:25 +09:00
nymkappa
ada18a0413 [accelerator] fix redirects 2024-04-16 17:02:10 +09:00
nymkappa
96d85dcacd [accelerator] polish pizza accel x2 2024-04-16 16:56:37 +09:00
nymkappa
64ba033602 [accelerator] polish pizza accel 2024-04-16 16:01:52 +09:00
nymkappa
26807e80db Merge branch 'nymkappa/accel-summary-default' into nymkappa/mini-branch 2024-04-16 14:43:01 +09:00
nymkappa
e4f00285b3 Merge branch 'nymkappa/polish-accel' into nymkappa/mini-branch 2024-04-16 14:42:53 +09:00
nymkappa
d443e51d30 [accelerator] if eligible, show pizza accel summary by default 2024-04-16 11:45:04 +09:00
softsimon
b4352f5f25 Merge pull request #4972 from mempool/dependabot/github_actions/dtolnay/rust-toolchain-bb45937a053e097f8591208d8e74c90db1873d07
Bump dtolnay/rust-toolchain from dc6353516c68da0f06325f42ad880f76a5e77ec9 to bb45937a053e097f8591208d8e74c90db1873d07
2024-04-15 18:10:12 +07:00
softsimon
2c1b4a2547 Merge pull request #4955 from mempool/dependabot/npm_and_yarn/backend/mysql2-3.9.4
Bump mysql2 from 3.9.1 to 3.9.4 in /backend
2024-04-15 18:09:56 +07:00
softsimon
b54685bed7 Merge pull request #4973 from mempool/natsoni/fix-dropdown-menu-color
Adapt dropdown menu color to theme
2024-04-15 18:02:54 +07:00
natsoni
9032d5ac11 Adapt dropdown menu color to theme 2024-04-15 12:35:15 +02:00
dependabot[bot]
942747a0be Bump dtolnay/rust-toolchain
Bumps [dtolnay/rust-toolchain](https://github.com/dtolnay/rust-toolchain) from dc6353516c68da0f06325f42ad880f76a5e77ec9 to bb45937a053e097f8591208d8e74c90db1873d07.
- [Release notes](https://github.com/dtolnay/rust-toolchain/releases)
- [Commits](dc6353516c...bb45937a05)

---
updated-dependencies:
- dependency-name: dtolnay/rust-toolchain
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-15 03:00:06 +00:00
nymkappa
fdd6463ac0 [ui] polish pizza accelerator 2024-04-14 16:29:56 +09:00
nymkappa
e9afc4ec0f remove duplicated condition 2024-04-14 13:55:49 +09:00
nymkappa
dff811f615 [accelerator] re-add missing accelerator button on mobile, set location.hash when click on accelerate button 2024-04-14 13:54:25 +09:00
wiz
d1e382ddf7 Merge pull request #4967 from mempool/mononaut/fix-acc-fee-rate-again
Fix accelerated fee rate again
2024-04-14 13:20:06 +09:00
wiz
ccd460cf70 Merge pull request #4968 from mempool/mononaut/54-year-pizza
Fix first seen loading time in tx tracker
2024-04-14 13:18:56 +09:00
Mononaut
7b1ba912be Fix first seen loading time in tx tracker 2024-04-14 03:49:26 +00:00
Mononaut
f3864c9100 Fix accelerated fee rate again 2024-04-14 03:21:24 +00:00
wiz
1082889b64 Merge pull request #4966 from mempool/nymkappa/fix-fee-paid
[accelerator] fix fee paid calculation
2024-04-14 00:20:42 +09:00
nymkappa
36839fdcb9 [accelerator] fix fee paid calculation 2024-04-14 00:19:14 +09:00
wiz
ebe54d47d8 Merge pull request #4965 from mempool/nymkappa/remove-prepaid-tx
[accelerator] remove simple mode from tx page
2024-04-14 00:15:02 +09:00
nymkappa
7219823847 [accelerator] remove simple mode from tx page 2024-04-14 00:10:15 +09:00
wiz
336fc7a64a Merge pull request #4964 from mempool/mononaut/bitcoin-tracker
Add Bitcoin logo to tracker header
2024-04-13 23:20:46 +09:00
Mononaut
84f62f8025 Add Bitcoin logo to tracker page 2024-04-13 14:18:01 +00:00
wiz
6bd6dfec49 Merge pull request #4963 from mempool/nymkappa/accel-checkout
[accelerator] remove test code
2024-04-13 23:13:29 +09:00
nymkappa
a6e27f1312 Merge branch 'master' into nymkappa/accel-checkout 2024-04-13 23:13:02 +09:00
nymkappa
c913df837a [accelerator] remove test code 2024-04-13 23:12:05 +09:00
wiz
42bd9811e3 Merge pull request #4962 from mempool/nymkappa/accel-checkout
[accelerator] polish prepaid accel UI
2024-04-13 23:09:03 +09:00
wiz
7fbf13fd9d Merge pull request #4961 from mempool/mononaut/pizza-layout
Change tx tracker layout
2024-04-13 23:08:10 +09:00
nymkappa
fb086b5ad5 [accelerator] polish UI prepaid accel 2024-04-13 23:07:19 +09:00
Mononaut
d4f51979d4 Change tx tracker layout 2024-04-13 13:56:29 +00:00
wiz
aeb54f59f1 Merge pull request #4960 from mempool/mononaut/change-toppings
change pizza tracker toppings
2024-04-13 22:17:10 +09:00
Mononaut
5ca3b24527 Fix accelerate arrow, add accelerating status 2024-04-13 13:14:26 +00:00
Mononaut
88df2878cb Add logo to tx tracker page, fix bugs 2024-04-13 12:26:30 +00:00
nymkappa
f601bbc499 Merge branch 'master' into nymkappa/accel-checkout 2024-04-13 21:13:11 +09:00
nymkappa
24e9ae6440 [accelerator] re-integrate square payment WIP 2024-04-13 20:53:19 +09:00
wiz
8c3f11622c Merge pull request #4959 from mempool/mononaut/pizza
WIP (work-in-pizza)
2024-04-13 19:02:22 +09:00
Mononaut
da25ed431f hijack /tx/ page for tracker users 2024-04-13 09:24:52 +00:00
Mononaut
ecc234a96a Tracker bottom panel with status icon 2024-04-13 09:09:14 +00:00
Mononaut
29851537eb Prototype accelerate checkout properties 2024-04-13 09:09:13 +00:00
nymkappa
5f2d7b32ae [accelerator] concept accelerator a/b ctas 2024-04-13 09:09:13 +00:00
Mononaut
de00d49d7b Basic tracker page structure 2024-04-13 09:09:09 +00:00
wiz
bafa0f50fc Merge pull request #4958 from mempool/wiz/20240413-add-new-lightning-nodes
Add new VA1/FRA lightning nodes
2024-04-13 17:56:13 +09:00
wiz
80c75f9aeb Add new FRA lightning nodes 2024-04-13 17:49:29 +09:00
wiz
407eba194d Add new VA1 lightning nodes 2024-04-13 17:37:54 +09:00
Mononaut
2eac3e6555 Add pizza tracker component 2024-04-13 08:05:31 +00:00
Mononaut
61a308cbc6 Add simplified tracker blockchain component 2024-04-13 08:04:58 +00:00
nymkappa
49b9a6f53d [accelerator] concept accelerator a/b ctas 2024-04-13 16:11:49 +09:00
wiz
db1cc0d0e1 Merge pull request #4953 from mempool/nymkappa/use-bid-boost-accel-list
[accelerator] use new bidBoost field
2024-04-13 14:59:33 +09:00
dependabot[bot]
faf79e85fd Bump mysql2 from 3.9.1 to 3.9.4 in /backend
Bumps [mysql2](https://github.com/sidorares/node-mysql2) from 3.9.1 to 3.9.4.
- [Release notes](https://github.com/sidorares/node-mysql2/releases)
- [Changelog](https://github.com/sidorares/node-mysql2/blob/master/Changelog.md)
- [Commits](https://github.com/sidorares/node-mysql2/compare/v3.9.1...v3.9.4)

---
updated-dependencies:
- dependency-name: mysql2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-12 16:14:46 +00:00
softsimon
0d72e88c6a Updated from transifex 2024-04-12 16:18:39 +09:00
Mononaut
786ec7fff1 Mobile transaction tracker page 2024-04-12 07:16:01 +00:00
softsimon
8df497e53d Merge pull request #4954 from mempool/simon/convert-paid-currency-api
Convert v3 Currency api into v1
2024-04-12 16:07:50 +09:00
softsimon
1dd7a6ebac Convert v3 api into v1 2024-04-12 15:59:33 +09:00
nymkappa
2c12e9f64b [accelerator] use new bidBoost field 2024-04-12 15:12:25 +09:00
softsimon
9a77135d30 Merge pull request #4952 from mempool/simon/paid-currency-api
Paid currency api support
2024-04-12 14:42:56 +09:00
softsimon
f3b3b9b3f0 Adding new config to sample 2024-04-12 14:15:50 +09:00
softsimon
55c4d4d03d Paid currency api support 2024-04-12 14:10:47 +09:00
softsimon
061d341d8b Merge pull request #4949 from mempool/mononaut/delta-sequence
Add sequence number to mempool block updates
2024-04-12 00:30:30 +09:00
softsimon
1c29b8b260 Merge pull request #4950 from mempool/natsoni/fix-timestamp-datetime
Update prices table  'time' field to datetime
2024-04-11 21:07:26 +09:00
natsoni
79bfe9c866 Change time field to datetime 2024-04-11 17:56:34 +09:00
Mononaut
3e6d38656d Add sequence number to mempool block updates 2024-04-11 08:11:45 +00:00
softsimon
5697486cea Merge pull request #4947 from mempool/mononaut/hide-accelerate-panel-already
Hide accelerate panel if not needed
2024-04-11 15:54:55 +09:00
softsimon
df0da605e4 Merge pull request #4948 from mempool/mononaut/54-years
it hasn't been 54 years since this transaction was submitted
2024-04-11 15:54:39 +09:00
Mononaut
fa9aaf0423 Keep firstSeen loader, retry on fail 2024-04-11 06:03:14 +00:00
Mononaut
89de288fec it hasn't been 54 years since this transaction was submitted 2024-04-11 04:02:25 +00:00
Mononaut
940003552b Hide acceleration panel if tx is already accelerated 2024-04-10 11:14:13 +00:00
Mononaut
0660296b51 Disable accelerate button on successful submission 2024-04-10 11:14:13 +00:00
softsimon
affeb0a169 Merge pull request #4943 from mempool/nymkappa/fix-missing-qr-code
[accelerator] fix missing qr code
2024-04-10 19:15:04 +09:00
softsimon
2504653426 Merge pull request #4207 from mempool/mononaut/sipper
Proof-of-concept "sipper" lightweight pages for web crawlers
2024-04-10 18:29:42 +09:00
softsimon
b6a9ad67d3 Merge pull request #4946 from mempool/mononaut/acc-dash-vbytes
Switch success rate to total vsize
2024-04-10 18:12:48 +09:00
Mononaut
3d4741eac2 Fix acceleration skeleton loader titles 2024-04-10 09:03:31 +00:00
softsimon
5611f57e9e Merge pull request #4945 from mempool/mononaut/acc-dash-times
Add timespan toggle to acceleration dashboard
2024-04-10 18:00:25 +09:00
softsimon
44027f5bc0 Pull from transifex 2024-04-10 17:02:35 +09:00
Mononaut
41b25a78e9 adjust accelerator dashboard mobile fields 2024-04-10 07:22:02 +00:00
Mononaut
e263f94765 Switch success rate for total vsize on acceleration dashboard 2024-04-10 06:52:54 +00:00
Mononaut
80c7754e48 Add timespan toggle to acceleration dashboard 2024-04-10 06:27:48 +00:00
softsimon
165340324c Merge pull request #4944 from mempool/natsoni/more-css-fix
More css fixes
2024-04-10 15:15:47 +09:00
softsimon
735dddd604 Merge pull request #4941 from mempool/natsoni/polish-footer
Polish global footer
2024-04-10 15:03:13 +09:00
natsoni
3e3bd32705 Convert more css to variables 2024-04-10 14:41:19 +09:00
softsimon
f83025a9ff Merge pull request #4939 from mempool/hunicus/natsoni-formatting
Make room for natsoni as project member
2024-04-10 14:40:10 +09:00
softsimon
ba6a7b58aa Merge pull request #4942 from mempool/natsoni/fix-header-color-theme
Fix header color theme
2024-04-10 14:37:56 +09:00
nymkappa
fb8808ea59 [accelerator] fix redirect url, fix UX 2024-04-10 14:33:33 +09:00
nymkappa
73f241e9c3 Revert "Disable accelerate button after submission"
This reverts commit e55b1740db.
2024-04-10 13:53:21 +09:00
softsimon
317b1b6ac5 Merge pull request #4909 from mempool/nymkappa/unix-socket
[server] express server also listens on unix socket
2024-04-10 13:41:14 +09:00
natsoni
cabe629f17 Add icon color variable to color themes 2024-04-10 12:26:00 +09:00
natsoni
9f6521b987 Update navbar background color 2024-04-10 12:25:30 +09:00
hunicus
6daffe5b13 Make room for natsoni as project member 2024-04-10 11:39:25 +09:00
nymkappa
4c7f93d1ef fix tests 2024-04-10 09:46:05 +09:00
natsoni
f81bbb93a5 Polish global footer 2024-04-09 20:55:14 +09:00
wiz
b0058e94ce Merge pull request #4938 from mempool/nymkappa/accel-graph
[accelerator] show 1w accel graph by default
2024-04-09 20:17:51 +09:00
nymkappa
de069f704a [accelerator] show 1w accel graph by default 2024-04-09 18:10:53 +09:00
wiz
bcb8493cd0 Merge pull request #4937 from mempool/mononaut/disable-acc-button
disable accelerate button after submission
2024-04-09 18:07:30 +09:00
Mononaut
e55b1740db Disable accelerate button after submission 2024-04-09 08:31:24 +00:00
softsimon
df673b2a4e Pull from transifex 2024-04-09 16:58:02 +09:00
wiz
a4753769d2 Merge pull request #4936 from mempool/natsoni/add-wiz-theme
Add wiz theme
2024-04-09 16:50:31 +09:00
wiz
be75a87e88 Merge pull request #4935 from mempool/mononaut/acc-rate-tooltips
Fix accelerated fee rate in mined block tooltips
2024-04-09 16:50:21 +09:00
nymkappa
0ac3ae1cb1 Update docker/backend/start.sh
Co-authored-by: softsimon <softsimon@users.noreply.github.com>
2024-04-09 16:50:04 +09:00
wiz
a5e1e95534 Fix capitalization of "wiz" theme 2024-04-09 16:49:41 +09:00
natsoni
7f6ab0b854 Add wiz theme 2024-04-09 16:43:02 +09:00
Mononaut
8420ecd380 Fix accelerated fee rate in mined block tooltips 2024-04-09 07:22:24 +00:00
softsimon
192fd09a00 Merge pull request #4933 from mempool/natsoni/fix-address-component-subscriptions
Fix subscription management in address component
2024-04-09 16:18:57 +09:00
softsimon
d9a59c6d1e Accelerator i18n fixes 2024-04-09 16:15:26 +09:00
softsimon
69c3c3162c Theme selector i18n fix 2024-04-09 16:08:58 +09:00
softsimon
61ba832dfd Merge pull request #4932 from mempool/natsoni/remove-console-log
Remove console log
2024-04-09 15:17:34 +09:00
natsoni
f332bba468 Remove console log 2024-04-09 15:14:50 +09:00
nymkappa
ba8cca6ba5 Merge branch 'master' into nymkappa/unix-socket 2024-04-09 15:11:52 +09:00
nymkappa
7a098952c8 [server] disable unix socket listening by default 2024-04-09 15:11:40 +09:00
softsimon
347b74a55d Theme elector width fix 2024-04-09 14:51:54 +09:00
wiz
d5b0adeeed Merge pull request #4931 from mempool/nymkappa/additional-error-message
add additional error message
2024-04-09 14:50:05 +09:00
softsimon
d8c4d36d4b Rename themes 2024-04-09 14:49:24 +09:00
nymkappa
aac32c5bff add additional error message 2024-04-09 14:33:18 +09:00
softsimon
ff86e55622 Merge pull request #4916 from mempool/natsoni/high-contrast-theme
High contrast theme (duplicate)
2024-04-09 14:33:09 +09:00
wiz
894c4cb139 Merge pull request #4930 from mempool/mononaut/filter-acc
Filter accelerations for matching pool
2024-04-09 14:32:27 +09:00
softsimon
2792016383 Fix relative imports 2024-04-09 14:24:34 +09:00
wiz
46215871aa Merge pull request #4928 from mempool/hunicus/bid-boost-link
Make bid boost widget link clickable on mobile
2024-04-09 14:24:28 +09:00
natsoni
cfc06be975 Fix subscription management in address component 2024-04-09 14:21:08 +09:00
natsoni
527589ac04 Merge branch 'master' into natsoni/high-contrast-theme 2024-04-09 12:13:11 +09:00
Mononaut
43845cda5c Filter accelerations for matching pool 2024-04-09 00:08:50 +00:00
wiz
b74b8a8a5a Merge pull request #4929 from mempool/mononaut/fix-acc-rate-labels
Fix accelerated rate labels
2024-04-08 23:13:20 +09:00
Mononaut
226c6d8432 More acceleration labelling fixes 2024-04-08 14:08:11 +00:00
Mononaut
9f79258dec Fix accelerated/effective rate labelling 2024-04-08 13:45:05 +00:00
Mononaut
13bcc99095 Fix block summary data fields 2024-04-08 13:44:08 +00:00
hunicus
fef9b93a05 Merge branch 'master' into hunicus/bid-boost-link 2024-04-08 22:17:10 +09:00
hunicus
ccac3437cf Make bid boost widget link clickable on mobile 2024-04-08 22:09:52 +09:00
wiz
e849a31668 Merge pull request #4926 from mempool/nymkappa/duplicated-accel
[accelerator] fix possible duplicated accel request call
2024-04-08 21:57:17 +09:00
nymkappa
88de5412f8 [accelerator] reset cashapp upon price update 2024-04-08 21:55:47 +09:00
wiz
d13c48d31d Merge pull request #4925 from mempool/simon/fix-first-seen-skeleton
Fix first seen skeleton loader
2024-04-08 21:55:31 +09:00
softsimon
4c807866a3 Fix first seen skeleton loader 2024-04-08 21:44:29 +09:00
nymkappa
d7a4a95c05 [accelerator] avoid duplicated accel request 2024-04-08 21:43:06 +09:00
wiz
b35422ff9f Merge pull request #4922 from mempool/mononaut/actually-fix-sticky-button
Actually fix sticky button
2024-04-08 21:32:28 +09:00
wiz
b6be5d71bb Merge pull request #4923 from mempool/mononaut/wider-sticky-button
Slightly wider sticky button
2024-04-08 21:32:12 +09:00
wiz
4c5eddcf6d Merge pull request #4924 from mempool/mononaut/fix-acc-fee-rate
Fix missing accelerated fee rate again
2024-04-08 21:31:27 +09:00
softsimon
51f0b75a64 Merge pull request #4917 from mempool/natsoni/fix-liquid-dropdown-position
Fix dropdown menu position in Liquid
2024-04-08 21:03:39 +09:00
softsimon
fe4648cd9e Update frontend/src/app/components/liquid-master-page/liquid-master-page.component.scss 2024-04-08 21:03:25 +09:00
softsimon
03867ada49 Merge pull request #4887 from mempool/mononaut/local-acceleration-data
Local acceleration data
2024-04-08 20:43:46 +09:00
Mononaut
7959188c06 Fix missing accelerated fee rate again 2024-04-08 11:36:22 +00:00
Mononaut
11eaa0ca50 Fix NaN boost 2024-04-08 11:22:57 +00:00
Mononaut
dc6dba416a Fix missing boost 2024-04-08 11:22:57 +00:00
Mononaut
c8e7cc773a Always use local data for pending/historical accelerations 2024-04-08 11:22:57 +00:00
Mononaut
efe43329a1 Support PREFER_LOCAL for /accelerations(/history) 2024-04-08 11:22:56 +00:00
Mononaut
3f97c17af2 Fetch block accelerations by height instead of hash 2024-04-08 11:22:56 +00:00
Mononaut
91493e8769 Roll back local acceleration data on reorg 2024-04-08 11:22:56 +00:00
softsimon
5f36cb88ab Merge pull request #4895 from mempool/hunicus/txacc-faq
Add note about accelerator waitlist to faq
2024-04-08 18:44:40 +09:00
Mononaut
a0ef635c92 Slightly wider sticky button 2024-04-08 09:36:45 +00:00
natsoni
d68904fec0 More contrast theme fixes 2024-04-08 18:32:59 +09:00
Mononaut
b679581cf2 Actually fix sticky button 2024-04-08 09:25:53 +00:00
softsimon
f7e6fa026d Merge pull request #4921 from mempool/mononaut/fix-accel-highlight
Fix new acceleration visualization color change
2024-04-08 18:04:23 +09:00
softsimon
96f16f1f2e Merge pull request #4914 from mempool/hunicus/why-no-confirm
Remove another faq reference to tx-acc
2024-04-08 18:01:34 +09:00
Mononaut
ad8fa8722f Fix new acceleration color change 2024-04-08 09:00:51 +00:00
wiz
e477f09cd5 Merge pull request #4920 from mempool/mononaut/fix-tx-autoscroll
Fix tx autoscroll
2024-04-08 17:59:54 +09:00
Mononaut
be5eb9ef70 Fix tx autoscroll 2024-04-08 08:51:35 +00:00
wiz
b952642570 Update staging URLs for prod Square SDK load 2024-04-08 17:12:38 +09:00
wiz
47cc74a351 Merge pull request #4913 from mempool/mononaut/simple-acceleration-mode
Simplified acceleration mode for mobile
2024-04-08 17:06:20 +09:00
wiz
cff572a104 Merge branch 'master' into mononaut/simple-acceleration-mode 2024-04-08 17:05:51 +09:00
Mononaut
48e16e64c2 Simple mode redesign w/ sticky button 2024-04-08 08:05:22 +00:00
wiz
46172836f1 Merge pull request #4915 from mempool/simon/fix-mobile-initial-zoom
Fix initial zoom behavior on mobile
2024-04-08 16:13:18 +09:00
wiz
eacb72a05b Merge pull request #4912 from mempool/mononaut/accelerated-fee-rates
Fix accelerated fee rates
2024-04-08 16:10:55 +09:00
softsimon
c1fefaab92 Pull from transifex 2024-04-08 16:01:26 +09:00
natsoni
f2f86457ee Fix dropdown menu position in Liquid 2024-04-08 15:53:51 +09:00
natsoni
c251b5831b Merge branch 'master' into natsoni/contrast-theme 2024-04-08 14:54:38 +09:00
softsimon
8c589d3000 Fix initial zoom behavior on mobile
fixes #4875
2024-04-08 14:54:33 +09:00
hunicus
ddc599f6b7 Remove another faq reference to tx-acc 2024-04-08 14:18:53 +09:00
hunicus
6822c3a04b Merge branch 'master' into hunicus/txacc-faq 2024-04-08 14:00:00 +09:00
Mononaut
aa0c70bd44 Simplified acceleration mode for mobile 2024-04-08 04:58:56 +00:00
hunicus
94f649b345 Add condition for officialmempoolspace 2024-04-08 13:50:48 +09:00
Mononaut
5e07e9dceb Fix accelerated fee rates 2024-04-08 03:03:26 +00:00
softsimon
91a8a8be34 Merge pull request #4911 from daweilv/master
fix(address): clicking on the Balance History chart within the /testnet/address/:id page would incorrectly navigate to /tx/:tx
2024-04-08 11:41:54 +09:00
natsoni
99e1890795 Fix footer to fit with theme selector 2024-04-08 10:50:59 +09:00
Metadavid
5583befbba Update daweilv.txt 2024-04-07 21:42:36 +08:00
Metadavid
c637055859 Update daweilv.txt 2024-04-07 21:20:42 +08:00
Metadavid
c3acfb8781 Update address-graph.component.ts
fix(address): Clicking on the Balance History chart within the /testnet/address/:id page under testnet now navigates to /testnet/tx/:tx instead of /tx/:tx.
2024-04-07 21:04:23 +08:00
Metadavid
fd073a7043 Add files via upload 2024-04-07 21:03:20 +08:00
nymkappa
bed00fbd41 [server] express server also listens on unix socket 2024-04-07 18:39:37 +09:00
softsimon
501b79fdce Italian and Spanish fineshed 2024-04-07 10:43:17 +09:00
softsimon
ed6af8f560 Merge pull request #4906 from mempool/mononaut/acceleration-harmony
Acceleration sound
2024-04-06 19:02:26 +09:00
Mononaut
32495736d4 such bright wow harmony 2024-04-06 09:58:07 +00:00
wiz
bff48b0a64 Merge pull request #4901 from mempool/nymkappa/clear-state
[accelerator] clear state after loading preview
2024-04-06 18:47:46 +09:00
softsimon
f6228240ba Transifex pull 2024-04-06 18:04:39 +09:00
softsimon
215c8a7ff4 Merge pull request #4902 from mempool/mononaut/disappearing-effective-rates
Fix disappearing effective/accelerated fee rates
2024-04-06 18:00:07 +09:00
softsimon
0f217bd753 Merge pull request #4903 from mempool/mononaut/bright-purple
Don't apply age tint to accelerated txs
2024-04-06 17:39:24 +09:00
natsoni
7e920f4bae Replace more hardcoded css 2024-04-06 17:14:53 +09:00
natsoni
cde3d878b1 Fix block overview graph on contrast theme 2024-04-06 15:59:42 +09:00
natsoni
621a6ea42d Fix rbf tree fade out 2024-04-06 15:59:36 +09:00
natsoni
cfc3615e64 Fix global footer selector css 2024-04-06 15:59:30 +09:00
natsoni
2f8d0d90cd Update theme-contrast.css 2024-04-06 15:59:26 +09:00
natsoni
c827953ca5 Fix remaining bugs from rebase 2024-04-06 15:59:07 +09:00
Mononaut
79dd263fb1 add high contrast theme 2024-04-06 15:56:53 +09:00
Mononaut
1ca05a029a add theme selector to main dashboard 2024-04-06 15:56:22 +09:00
Mononaut
4c205eb09d implement theme switching service 2024-04-06 15:55:47 +09:00
natsoni
ee92f6639a convert to CSS variables 2024-04-06 15:54:55 +09:00
Mononaut
7721f16f9f Don't apply age tint to accelerated txs 2024-04-06 03:44:58 +00:00
nymkappa
854222b8cc [accelerator] clear state after loading preview 2024-04-06 12:32:33 +09:00
Mononaut
0bc86541c6 Fix disappearing effective/accelerated fee rates 2024-04-06 03:22:56 +00:00
softsimon
c45111333d Fix space 2024-04-06 11:58:48 +09:00
softsimon
ba6fedc430 Merge pull request #4900 from mempool/nymkappa/fix-accel-preview-autoscroll
[accelerator] fix accel preview autoscroll
2024-04-05 21:06:57 +09:00
nymkappa
caa34fa8bb [accelerator] fix accel preview autoscroll 2024-04-05 20:59:15 +09:00
softsimon
fa7c0ab58e Pull from transifex 2024-04-05 20:46:37 +09:00
wiz
2543eb6861 Merge pull request #4899 from mempool/nymkappa/polish-button
[accelerator] fix label accelerate button
2024-04-05 20:45:27 +09:00
nymkappa
c4cf3363ee [accelerator] fix label accelerate button 2024-04-05 20:39:43 +09:00
softsimon
98d4efd509 Merge pull request #4889 from mempool/mononaut/non-esplora-inscription-goggles
Add support for inscription goggles for non-esplora backends
2024-04-05 18:28:07 +09:00
softsimon
a99e65ee48 Merge branch 'master' into mononaut/non-esplora-inscription-goggles 2024-04-05 18:28:01 +09:00
wiz
3fd4d24c93 Merge pull request #4897 from mempool/nymkappa/fix-preview-loading
[accelerator] fix spinner loading
2024-04-05 18:23:38 +09:00
nymkappa
55a564a5a8 [accelerator] fix spinner loading 2024-04-05 17:52:07 +09:00
softsimon
04f6490eeb Merge branch 'master' into mononaut/non-esplora-inscription-goggles 2024-04-05 17:48:55 +09:00
softsimon
b5026789d6 Merge pull request #4896 from mempool/nymkappa/fix-referrer
[accelator] fix referrer check
2024-04-05 17:38:47 +09:00
nymkappa
705e570cf5 [accelator] fix referrer check 2024-04-05 17:36:04 +09:00
softsimon
10a41fb0d1 Merge pull request #4804 from mempool/nymkappa/prepaid-acceleration
[accelerator] prepaid acceleration
2024-04-05 17:32:46 +09:00
hunicus
2a591646c3 Add note about accelerator waitlist to faq 2024-04-05 16:53:19 +09:00
softsimon
e55898b414 Merge pull request #4891 from mempool/mononaut/tx-details-stripes
Transaction details template refactor
2024-04-05 16:50:41 +09:00
Mononaut
2df476406d Transaction details template refactor 2024-04-05 07:46:48 +00:00
softsimon
4a05b35f2b Pulling from transifex 2024-04-05 16:46:23 +09:00
softsimon
3ad8e56c25 Accelerations i18n key fix 2024-04-05 16:30:26 +09:00
softsimon
c1704758fd Merge pull request #4893 from mempool/simon/build-target-es2020
Set build target to es2020
2024-04-05 16:26:38 +09:00
softsimon
fe580f2b2b Merge pull request #4894 from mempool/hunicus/esplora-docs
Switch blockstream/electrs for mempool/electrs in backend readme
2024-04-05 16:19:35 +09:00
hunicus
d82e482acc Update readme: blockstream/electrs for mempool/electrs 2024-04-05 15:59:16 +09:00
softsimon
9d6142dc79 Merge pull request #4892 from mempool/hunicus/memsplora
Switch blockstream/electrs for mempool/electrs in faq
2024-04-05 15:57:14 +09:00
softsimon
2dec83735b Set build target to es2020 2024-04-05 15:56:10 +09:00
softsimon
9f4204b815 Remove goggles i18n 2024-04-05 15:50:34 +09:00
hunicus
40f2c972d7 Update faq: blockstream/electrs for mempool/electrs 2024-04-05 15:50:15 +09:00
softsimon
eee2385d0d Merge pull request #4886 from mempool/nymkappa/mempool-goggle-tm
[copy] add missing trademark to Mempool Goggles
2024-04-05 14:48:43 +09:00
softsimon
492c652157 Merge pull request #4885 from mempool/natsoni/fix-side-nav-on-rtl
Fix side panel for rtl languages
2024-04-05 14:47:36 +09:00
softsimon
af8e060dc6 A few goggles i18n 2024-04-05 12:37:17 +09:00
softsimon
6a19b9058f Merge pull request #4888 from mempool/mononaut/tint
Goggles age tint by default
2024-04-05 12:36:24 +09:00
nymkappa
bcbd21b922 [acclerator] load square for prepaid acceleration 2024-04-05 11:16:48 +09:00
nymkappa
aac76d68b0 Merge branch 'master' into nymkappa/prepaid-acceleration 2024-04-05 11:14:16 +09:00
softsimon
25bb942e04 Fixing the other i18n 2024-04-04 21:55:14 +09:00
softsimon
8ac71f21d1 Updating i18n for accelerator preview 2024-04-04 21:48:16 +09:00
Mononaut
287559f028 Add support for inscription goggles for non-esplora backends 2024-04-04 12:32:03 +00:00
Mononaut
7fbb93cf41 Goggles age tint by default 2024-04-04 11:33:51 +00:00
nymkappa
7b76e93631 Update messages.ca.xlf 2024-04-04 18:26:38 +08:00
wiz
896c451c3e Merge pull request #4884 from mempool/nymkappa/about-page 2024-04-04 19:18:48 +09:00
natsoni
0874386001 Fix side panel for rtl languages 2024-04-04 19:00:04 +09:00
nymkappa
ce6808b3c4 [copy] add missing trademark to Mempool Goggles 2024-04-04 18:01:01 +09:00
softsimon
40fa1c5acb Some language string updates 2024-04-04 17:51:42 +09:00
nymkappa
7307990f6f [about page] enable sponsors on self hosted 2024-04-04 17:36:37 +09:00
softsimon
5104da500e Adding i18n to mempool goggles 2024-04-04 17:28:32 +09:00
softsimon
3a8c46bbed Merge pull request #4837 from mempool/mononaut/goggles-age-filter
Goggles age filter
2024-04-04 16:47:34 +09:00
Mononaut
fc5312549d Switch gradient toggle fee -> default 2024-04-04 07:42:31 +00:00
softsimon
c14e8797e2 Merge pull request #4879 from mempool/nymkappa/proxy-accel-endpoints
[accelerator] proxy acceleration api to prod
2024-04-04 16:20:27 +09:00
nymkappa
0f26940018 Merge branch 'master' into nymkappa/proxy-accel-endpoints 2024-04-04 16:01:17 +09:00
Mononaut
26227e2f3b New opacity-based age Goggles 2024-04-04 06:57:38 +00:00
Mononaut
dcf78fab06 Block visualization color-by-age mode 2024-04-04 06:57:37 +00:00
softsimon
0813592a6d Merge pull request #4882 from mempool/nymkappa/reset-pool-sha
[migration] reset mining pool sha to force refreshing
2024-04-04 15:20:04 +09:00
nymkappa
abdb27af3f [migration] reset mining pool sha to force refreshing 2024-04-04 14:27:50 +09:00
nymkappa
b421be3315 [accelerator] also forward headers 2024-04-04 14:04:12 +09:00
softsimon
b74fbee069 Merge pull request #4881 from mempool/nymkappa/proxy-sponsors
[about page] proxy community sponsors apis to prod, small refactor
2024-04-04 14:02:00 +09:00
nymkappa
dab9357b40 [about page] proxy community sponsors apis to prod, small refactor 2024-04-04 13:56:39 +09:00
softsimon
ccd89604c0 Merge pull request #4856 from mempool/hunicus/docs-links-alignment
Fix api-docs anchor link vertical alignment
2024-04-04 13:54:53 +09:00
Felipe Knorr Kuhn
cd135b7171 Merge branch 'master' into nymkappa/proxy-accel-endpoints 2024-04-04 13:14:54 +09:00
softsimon
7e16d550b0 Merge pull request #4880 from mempool/knorrium/enable_fiat_price_again
Enable fiat price conversion again
2024-04-04 13:14:24 +09:00
nymkappa
404079ef4e [accelerator] use config.MEMPOOL_SERVICES.API url 2024-04-04 13:10:32 +09:00
Felipe Knorr Kuhn
d13f78f046 Enable fiat price conversion again 2024-04-04 13:10:11 +09:00
nymkappa
60040c3914 [accelerator] proxy acceleration api to prod 2024-04-04 12:57:54 +09:00
softsimon
e408fbd8d6 Merge branch 'master' into hunicus/docs-links-alignment 2024-04-04 12:40:14 +09:00
softsimon
6931ecd468 Merge pull request #4878 from mempool/dependabot/npm_and_yarn/backend/multi-b37afcb6a9
Bump ws and @types/ws in /backend
2024-04-04 12:31:27 +09:00
dependabot[bot]
ed5d30ea5b Bump ws and @types/ws in /backend
Bumps [ws](https://github.com/websockets/ws) and [@types/ws](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/ws). These dependencies needed to be updated together.

Updates `ws` from 8.13.0 to 8.16.0
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.13.0...8.16.0)

Updates `@types/ws` from 8.5.5 to 8.5.10
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/ws)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: "@types/ws"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-04 02:49:18 +00:00
wiz
ead7613579 Merge pull request #4877 from mempool/nymkappa/truncate-hashrate-difficulty
[migration] re-index hashrates and difficulty_adjustments
2024-04-04 11:05:16 +09:00
wiz
ca0613ec17 Merge pull request #4876 from mempool/knorrium/change_default_replication_settings
Update default settings for Docker
2024-04-04 11:05:04 +09:00
nymkappa
727208ff84 [migration] re-index hashrates and difficulty_adjustments 2024-04-04 10:57:54 +09:00
Felipe Knorr Kuhn
53c3de2af5 Update default settings for Docker 2024-04-04 10:49:32 +09:00
softsimon
3f50d57ed1 Fix test broke due to i18n key change 2024-04-03 21:44:48 +09:00
softsimon
55f9d0f875 Merge branch 'master' into hunicus/docs-links-alignment 2024-04-03 18:57:46 +09:00
softsimon
37cb9b0fe8 Merge pull request #4872 from mempool/natsoni/fix-fiat-selector-sorting
Sort currency by ticker in fiat selector
2024-04-03 18:57:24 +09:00
softsimon
a3b5f79094 Merge pull request #4871 from mempool/natsoni/fix-liquid-testnet
Fix Liquid testnet broken blocks
2024-04-03 18:55:55 +09:00
softsimon
1b1ffa7109 Transifex pull 04-03 2024-04-03 18:47:16 +09:00
softsimon
2b3021c8fe Merge pull request #4873 from mempool/simon/i18n-improvements-04-02
i18n corrections and merging
2024-04-03 18:18:29 +09:00
softsimon
0ccc47786d Merge pull request #4874 from mempool/knorrium/fix_docker_for_rust
Fix the Docker workflow for the new Rust steps
2024-04-03 18:17:06 +09:00
softsimon
24366b929e i18n corrections and merging 2024-04-03 18:13:42 +09:00
Felipe Knorr Kuhn
2cab2a7885 Read FD from the environment variables 2024-04-03 18:13:14 +09:00
Felipe Knorr Kuhn
4284038c4b Fix error when deleting the missing @napi-rs folder 2024-04-03 18:12:51 +09:00
Felipe Knorr Kuhn
425d158777 Update Dockerfile to work with the new Rust steps 2024-04-03 18:12:26 +09:00
Felipe Knorr Kuhn
1a11b1813f Add more build contexts for the backend and rust-gbt 2024-04-03 18:11:58 +09:00
wiz
85a86d4b06 ops: Add backend git hash check 2024-04-03 17:28:58 +09:00
natsoni
073578243c Fix fiat selector sorting 2024-04-03 16:34:56 +09:00
wiz
69578f8086 Merge pull request #4820 from mempool/nymkappa/polish-enterprise-logo
[enterprise] polish enterprise logo
2024-04-03 16:17:37 +09:00
wiz
ec92f83a58 Merge pull request #4865 from mempool/mononaut/public-acceleration-config
Public acceleration config
2024-04-03 16:16:36 +09:00
wiz
37c0adbbfa Merge pull request #4868 from mempool/natsoni/fix-global-footer
Fix footer position
2024-04-03 16:15:57 +09:00
natsoni
e9c40692a6 Fix network switch mechanism 2024-04-03 16:09:30 +09:00
softsimon
32383b0bc2 Merge pull request #4869 from mempool/knorrium/more_bisq_cleanup
Cleanup more bisq references
2024-04-03 14:01:53 +09:00
Felipe Knorr Kuhn
d2f912d15c Remove leftover Bisq references from the GH workflow 2024-04-03 12:31:41 +09:00
Felipe Knorr Kuhn
c35c08b17a Remove bisq references from the tests 2024-04-03 12:31:17 +09:00
natsoni
4efd0dda83 Push footer down when content is less than window height 2024-04-03 11:28:25 +09:00
wiz
e7f8bcf83b ops: Update check script for elements electrs hash 2024-04-02 22:24:00 +09:00
wiz
08fa6d55b5 Merge pull request #4832 from mempool/dependabot/npm_and_yarn/frontend/express-4.19.2
Bump express from 4.18.2 to 4.19.2 in /frontend
2024-04-02 22:03:05 +09:00
Felipe Knorr Kuhn
995a26b944 Merge branch 'master' into nymkappa/prepaid-acceleration 2024-04-02 21:53:01 +09:00
Felipe Knorr Kuhn
3a2aad645b Merge branch 'master' into mononaut/public-acceleration-config 2024-04-02 21:31:28 +09:00
Felipe Knorr Kuhn
2c655a08be Merge branch 'master' into dependabot/npm_and_yarn/frontend/express-4.19.2 2024-04-02 21:13:34 +09:00
softsimon
bd359fdd31 Merge pull request #4859 from mempool/nymkappa/accel-copy
[accelerator] deposit -> top up
2024-04-02 21:12:47 +09:00
Felipe Knorr Kuhn
13216687dc Merge branch 'master' into nymkappa/accel-copy 2024-04-02 21:03:41 +09:00
wiz
1bf4f9902e Merge pull request #4862 from mempool/knorrium/fix_liquid_tests
Re-enable and fix Liquid tests
2024-04-02 21:02:42 +09:00
Felipe Knorr Kuhn
9908ec01aa Merge branch 'master' into mononaut/public-acceleration-config 2024-04-02 20:53:01 +09:00
Felipe Knorr Kuhn
051533bb14 Merge branch 'master' into knorrium/fix_liquid_tests 2024-04-02 20:48:36 +09:00
Mononaut
3d900bdfe5 Enable PUBLIC_ACCELERATIONS in mainnet prod 2024-04-02 08:24:57 +00:00
Mononaut
b32cce1440 Restrict public acceleration APIs to mainnet 2024-04-02 08:23:38 +00:00
Mononaut
37aee77eb4 Add config for public acceleration data 2024-04-02 08:19:35 +00:00
softsimon
72b8ae2a55 Revert id change 2024-04-02 17:07:18 +09:00
Felipe Knorr Kuhn
3e3bed9aa4 Disable viewport tests for now 2024-04-02 16:54:02 +09:00
Felipe Knorr Kuhn
844a86f997 Merge branch 'master' into knorrium/fix_liquid_tests 2024-04-02 16:44:04 +09:00
Felipe Knorr Kuhn
468f17c76c Add a few more Liquid testnet tests 2024-04-02 16:18:52 +09:00
softsimon
68a7a89fd6 Merge pull request #4864 from mempool/simon/update-i18n-04-02
Simon/update i18n 04 02
2024-04-02 16:06:07 +09:00
softsimon
931551a17f New extract 2024-04-02 16:05:52 +09:00
softsimon
7b79384f27 Extracting i18n 2024-04-02 16:02:18 +09:00
softsimon
69568421f3 Merge pull request #4232 from mempool/hunicus/move-on-in-it
Fix mining dashboard meta description not showing
2024-04-02 15:58:07 +09:00
softsimon
7fab57193d Merge pull request #4863 from mempool/mononaut/fix-block-page
Fix block audit cache bug
2024-04-02 15:32:58 +09:00
Mononaut
49146e1402 Fix mainnet blocks loading bug 2024-04-02 06:29:43 +00:00
hunicus
f28abd68f4 Merge branch 'master' into hunicus/move-on-in-it 2024-04-02 15:26:11 +09:00
Felipe Knorr Kuhn
4bb1320563 Re-enable and fix Liquid tests 2024-04-02 15:14:02 +09:00
dependabot[bot]
0a848f25a4 Bump express from 4.18.2 to 4.19.2 in /frontend
Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-02 06:07:47 +00:00
wiz
83dbae3a70 Merge pull request #4786 from mempool/simon/remove-bisq
Removing Bisq
2024-04-02 15:06:23 +09:00
wiz
ce057bad20 ops: Bump NodeJS to v20.12.0 in start script 2024-04-02 14:58:55 +09:00
wiz
8bec0cacdb ops: Remove bisq from start script 2024-04-02 14:58:21 +09:00
wiz
faa990a2c6 ops: Remove bisq from build script 2024-04-02 14:55:52 +09:00
wiz
872e4b4a17 Merge pull request #4855 from mempool/knorrium/fix_missing_assets
Run sync-assets-dev when building
2024-04-02 14:55:03 +09:00
softsimon
3e0c36e714 Merge pull request #4854 from mempool/natsoni/fix-network-errors
Fix frontend errors on block loading
2024-04-02 14:38:26 +09:00
softsimon
974eaeb02f Merge branch 'master' into natsoni/fix-network-errors 2024-04-02 14:31:32 +09:00
softsimon
f833904d7b Merge pull request #4858 from mempool/mononaut/better-audits
Betterer audits
2024-04-02 14:30:17 +09:00
Mononaut
96ba7d0656 Add draft sip tx page 2024-04-02 05:28:03 +00:00
Mononaut
6852319e4d Improve sip API error handling 2024-04-02 05:26:27 +00:00
Mononaut
8bc1eaebc0 Sip set /block canonical url 2024-04-02 05:26:27 +00:00
Mononaut
eeefaa6374 Proof-of-concept "sipper" minimal pages for web crawlers 2024-04-02 05:26:24 +00:00
nymkappa
1bba1cfeb1 [accelerator] deposit -> top up 2024-04-02 11:35:03 +08:00
hunicus
fb6aec0afe Merge branch 'master' into hunicus/docs-links-alignment 2024-04-01 19:06:18 +09:00
Felipe Knorr Kuhn
7063d9e113 Merge branch 'master' into knorrium/fix_missing_assets 2024-04-01 19:04:06 +09:00
Felipe Knorr Kuhn
0445c150a2 Run sync-assets-dev when building 2024-04-01 19:01:08 +09:00
wiz
cfb1e1d908 ops: Remove nginx config for bisq 2024-04-01 19:00:37 +09:00
natsoni
3f80dbc5ce Fix random console errors on block loading 2024-04-01 18:53:50 +09:00
hunicus
b8a48314c1 Fix api-docs anchor link vertical alignment 2024-04-01 18:53:38 +09:00
softsimon
0090d36514 Merge branch 'master' into simon/remove-bisq 2024-04-01 18:52:56 +09:00
nymkappa
f4b9301f55 [enterprise] polish enterprise logo 2024-03-26 11:36:20 +09:00
softsimon
60996a99f0 Removing Bisq 2024-03-23 18:03:49 +09:00
nymkappa
1eb52d8a35 [accelerator] fix redirection link 2024-03-21 19:08:48 +09:00
nymkappa
8fee195577 [accelerator] prepaid acceleration 2024-03-21 16:44:07 +09:00
hunicus
89d805b500 Merge branch 'master' into hunicus/move-on-in-it 2024-01-26 08:40:40 -05:00
hunicus
572f6bdbac Fix mining dashboard meta description not showing 2023-09-06 22:42:58 +09:00
hunicus
42a222338b Add missing meta descriptions 2023-09-06 22:42:33 +09:00
413 changed files with 92947 additions and 86530 deletions

View File

@@ -35,7 +35,7 @@ jobs:
- name: Install ${{ steps.gettoolchain.outputs.toolchain }} Rust toolchain
# Latest version available on this commit is 1.71.1
# Commit date is Aug 3, 2023
uses: dtolnay/rust-toolchain@dc6353516c68da0f06325f42ad880f76a5e77ec9
uses: dtolnay/rust-toolchain@bb45937a053e097f8591208d8e74c90db1873d07
with:
toolchain: ${{ steps.gettoolchain.outputs.toolchain }}
@@ -251,7 +251,6 @@ jobs:
strategy:
fail-fast: false
matrix:
# module: ["mempool", "liquid", "bisq"] Disabling bisq support for now
module: ["mempool", "liquid"]
include:
- module: "mempool"
@@ -263,9 +262,6 @@ jobs:
spec: |
cypress/e2e/liquid/liquid.spec.ts
cypress/e2e/liquidtestnet/liquidtestnet.spec.ts
# - module: "bisq"
# spec: |
# cypress/e2e/bisq/bisq.spec.ts
name: E2E tests for ${{ matrix.module }}
steps:

View File

@@ -101,5 +101,7 @@ jobs:
--platform linux/amd64,linux/arm64 \
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:latest \
--build-context rustgbt=./rust \
--build-context backend=./backend \
--output "type=registry" ./${{ matrix.service }}/ \
--build-arg commitHash=$SHORT_SHA

View File

@@ -103,7 +103,7 @@ In particular, make sure:
- the correct Bitcoin Core RPC credentials are specified in `CORE_RPC`
- the correct `BACKEND` is specified in `MEMPOOL`:
- "electrum" if you're using [romanz/electrs](https://github.com/romanz/electrs) or [cculianu/Fulcrum](https://github.com/cculianu/Fulcrum)
- "esplora" if you're using [Blockstream/electrs](https://github.com/Blockstream/electrs)
- "esplora" if you're using [mempool/electrs](https://github.com/mempool/electrs)
- "none" if you're not using any Electrum Server
### 6. Run Mempool Backend

View File

@@ -35,7 +35,8 @@
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
"ALLOW_UNREACHABLE": true,
"PRICE_UPDATES_PER_HOUR": 1,
"MAX_TRACKED_ADDRESSES": 100
"MAX_TRACKED_ADDRESSES": 100,
"UNIX_SOCKET_PATH": ""
},
"CORE_RPC": {
"HOST": "127.0.0.1",
@@ -97,10 +98,6 @@
"GEOLITE2_ASN": "/usr/local/share/GeoIP/GeoLite2-ASN.mmdb",
"GEOIP2_ISP": "/usr/local/share/GeoIP/GeoIP2-ISP.mmdb"
},
"BISQ": {
"ENABLED": false,
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db"
},
"LIGHTNING": {
"ENABLED": false,
"BACKEND": "lnd",
@@ -131,9 +128,7 @@
"MEMPOOL_API": "https://mempool.space/api/v1",
"MEMPOOL_ONION": "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1",
"LIQUID_API": "https://liquid.network/api/v1",
"LIQUID_ONION": "http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1",
"BISQ_URL": "https://bisq.markets/api",
"BISQ_ONION": "http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api"
"LIQUID_ONION": "http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1"
},
"REDIS": {
"ENABLED": false,
@@ -157,6 +152,7 @@
},
"FIAT_PRICE": {
"ENABLED": true,
"PAID": false,
"API_KEY": "your-api-key-from-freecurrencyapi.com"
}
}

View File

@@ -3,7 +3,7 @@ set -e
# Cleaning up inside the node_modules folder
cd package/node_modules
rm -r \
rm -rf \
typescript \
@typescript-eslint \
@napi-rs

View File

@@ -18,12 +18,12 @@
"crypto-js": "~4.2.0",
"express": "~4.19.2",
"maxmind": "~4.3.11",
"mysql2": "~3.9.1",
"mysql2": "~3.9.4",
"redis": "^4.6.6",
"rust-gbt": "file:./rust-gbt",
"socks-proxy-agent": "~7.0.0",
"typescript": "~4.9.3",
"ws": "~8.13.0"
"ws": "~8.16.0"
},
"devDependencies": {
"@babel/code-frame": "^7.18.6",
@@ -32,7 +32,7 @@
"@types/crypto-js": "^4.1.1",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.0",
"@types/ws": "~8.5.5",
"@types/ws": "~8.5.10",
"@typescript-eslint/eslint-plugin": "^5.55.0",
"@typescript-eslint/parser": "^5.55.0",
"eslint": "^8.36.0",
@@ -1858,9 +1858,9 @@
"dev": true
},
"node_modules/@types/ws": {
"version": "8.5.5",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz",
"integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==",
"version": "8.5.10",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
"integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==",
"dev": true,
"dependencies": {
"@types/node": "*"
@@ -6197,9 +6197,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/mysql2": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.1.tgz",
"integrity": "sha512-3njoWAAhGBYy0tWBabqUQcLtczZUxrmmtc2vszQUekg3kTJyZ5/IeLC3Fo04u6y6Iy5Sba7pIIa2P/gs8D3ZeQ==",
"version": "3.9.4",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.4.tgz",
"integrity": "sha512-OEESQuwxMza803knC1YSt7NMuc1BrK9j7gZhCSs2WAyxr1vfiI7QLaLOKTh5c9SWGz98qVyQUbK8/WckevNQhg==",
"dependencies": {
"denque": "^2.1.0",
"generate-function": "^2.3.1",
@@ -7690,9 +7690,9 @@
}
},
"node_modules/ws": {
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz",
"integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
"engines": {
"node": ">=10.0.0"
},
@@ -9198,9 +9198,9 @@
"dev": true
},
"@types/ws": {
"version": "8.5.5",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz",
"integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==",
"version": "8.5.10",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
"integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==",
"dev": true,
"requires": {
"@types/node": "*"
@@ -12382,9 +12382,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"mysql2": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.1.tgz",
"integrity": "sha512-3njoWAAhGBYy0tWBabqUQcLtczZUxrmmtc2vszQUekg3kTJyZ5/IeLC3Fo04u6y6Iy5Sba7pIIa2P/gs8D3ZeQ==",
"version": "3.9.4",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.4.tgz",
"integrity": "sha512-OEESQuwxMza803knC1YSt7NMuc1BrK9j7gZhCSs2WAyxr1vfiI7QLaLOKTh5c9SWGz98qVyQUbK8/WckevNQhg==",
"requires": {
"denque": "^2.1.0",
"generate-function": "^2.3.1",
@@ -13424,9 +13424,9 @@
}
},
"ws": {
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz",
"integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
"requires": {}
},
"y18n": {

View File

@@ -47,12 +47,12 @@
"crypto-js": "~4.2.0",
"express": "~4.19.2",
"maxmind": "~4.3.11",
"mysql2": "~3.9.1",
"mysql2": "~3.9.4",
"rust-gbt": "file:./rust-gbt",
"redis": "^4.6.6",
"socks-proxy-agent": "~7.0.0",
"typescript": "~4.9.3",
"ws": "~8.13.0"
"ws": "~8.16.0"
},
"devDependencies": {
"@babel/code-frame": "^7.18.6",
@@ -61,7 +61,7 @@
"@types/crypto-js": "^4.1.1",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.0",
"@types/ws": "~8.5.5",
"@types/ws": "~8.5.10",
"@typescript-eslint/eslint-plugin": "^5.55.0",
"@typescript-eslint/parser": "^5.55.0",
"eslint": "^8.36.0",

View File

@@ -7,6 +7,7 @@
"BLOCKS_SUMMARIES_INDEXING": true,
"GOGGLES_INDEXING": false,
"HTTP_PORT": 1,
"UNIX_SOCKET_PATH": "/mempool/socket/mempool-bitcoin-mainnet",
"SPAWN_CLUSTER_PROCS": 2,
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
"AUTOMATIC_BLOCK_REINDEXING": false,
@@ -93,10 +94,6 @@
"ENABLED": false,
"TX_PER_SECOND_SAMPLE_PERIOD": 20
},
"BISQ": {
"ENABLED": true,
"DATA_PATH": "__BISQ_DATA_PATH__"
},
"SOCKS5PROXY": {
"ENABLED": true,
"USE_ONION": true,
@@ -109,9 +106,7 @@
"MEMPOOL_API": "__EXTERNAL_DATA_SERVER_MEMPOOL_API__",
"MEMPOOL_ONION": "__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__",
"LIQUID_API": "__EXTERNAL_DATA_SERVER_LIQUID_API__",
"LIQUID_ONION": "__EXTERNAL_DATA_SERVER_LIQUID_ONION__",
"BISQ_URL": "__EXTERNAL_DATA_SERVER_BISQ_URL__",
"BISQ_ONION": "__EXTERNAL_DATA_SERVER_BISQ_ONION__"
"LIQUID_ONION": "__EXTERNAL_DATA_SERVER_LIQUID_ONION__"
},
"LIGHTNING": {
"ENABLED": true,
@@ -149,6 +144,7 @@
},
"FIAT_PRICE": {
"ENABLED": true,
"PAID": false,
"API_KEY": "__MEMPOOL_CURRENCY_API_KEY__"
}
}

View File

@@ -20,6 +20,7 @@ describe('Mempool Backend Config', () => {
BLOCKS_SUMMARIES_INDEXING: false,
GOGGLES_INDEXING: false,
HTTP_PORT: 8999,
UNIX_SOCKET_PATH: '',
SPAWN_CLUSTER_PROCS: 0,
API_URL_PREFIX: '/api/v1/',
AUTOMATIC_BLOCK_REINDEXING: false,
@@ -107,8 +108,6 @@ describe('Mempool Backend Config', () => {
expect(config.STATISTICS).toStrictEqual({ ENABLED: true, TX_PER_SECOND_SAMPLE_PERIOD: 150 });
expect(config.BISQ).toStrictEqual({ ENABLED: false, DATA_PATH: '/bisq/statsnode-data/btc_mainnet/db' });
expect(config.SOCKS5PROXY).toStrictEqual({
ENABLED: false,
USE_ONION: true,
@@ -122,9 +121,7 @@ describe('Mempool Backend Config', () => {
MEMPOOL_API: 'https://mempool.space/api/v1',
MEMPOOL_ONION: 'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1',
LIQUID_API: 'https://liquid.network/api/v1',
LIQUID_ONION: 'http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1',
BISQ_URL: 'https://bisq.markets/api',
BISQ_ONION: 'http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api'
LIQUID_ONION: 'http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1'
});
expect(config.MAXMIND).toStrictEqual({
@@ -154,6 +151,7 @@ describe('Mempool Backend Config', () => {
expect(config.FIAT_PRICE).toStrictEqual({
ENABLED: true,
PAID: false,
API_KEY: '',
});
});
@@ -182,8 +180,6 @@ describe('Mempool Backend Config', () => {
expect(config.STATISTICS).toStrictEqual(fixture.STATISTICS);
expect(config.BISQ).toStrictEqual(fixture.BISQ);
expect(config.SOCKS5PROXY).toStrictEqual(fixture.SOCKS5PROXY);
expect(config.EXTERNAL_DATA_SERVER).toStrictEqual(fixture.EXTERNAL_DATA_SERVER);

View File

@@ -0,0 +1,87 @@
import { Application } from "express";
import config from "../config";
import axios from "axios";
import logger from "../logger";
class AboutRoutes {
public initRoutes(app: Application) {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations`, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'donations/images/:id', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations/images/${req.params.id}`, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors`, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors/images/:id', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors/images/${req.params.id}`, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'translators', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators`, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'translators/images/:id', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators/images/${req.params.id}`, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'services/sponsors', async (req, res) => {
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
try {
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
logger.err(`Unable to fetch sponsors from ${url}. ${e}`, 'About Page');
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'services/account/images/:username', async (req, res) => {
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
try {
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
logger.err(`Unable to fetch sponsor profile image from ${url}. ${e}`, 'About Page');
res.status(500).end();
}
})
;
}
}
export default new AboutRoutes();

View File

@@ -0,0 +1,69 @@
import { Application, Request, Response } from 'express';
import config from '../../config';
import axios from 'axios';
import logger from '../../logger';
import mempool from '../mempool';
import AccelerationRepository from '../../repositories/AccelerationRepository';
class AccelerationRoutes {
private tag = 'Accelerator';
public initRoutes(app: Application): void {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations', this.$getAcceleratorAccelerations.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/history', this.$getAcceleratorAccelerationsHistory.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/history/aggregated', this.$getAcceleratorAccelerationsHistoryAggregated.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/stats', this.$getAcceleratorAccelerationsStats.bind(this))
;
}
private async $getAcceleratorAccelerations(req: Request, res: Response): Promise<void> {
const accelerations = mempool.getAccelerations();
res.status(200).send(Object.values(accelerations));
}
private async $getAcceleratorAccelerationsHistory(req: Request, res: Response): Promise<void> {
const history = await AccelerationRepository.$getAccelerationInfo(null, req.query.blockHeight ? parseInt(req.query.blockHeight as string, 10) : null);
res.status(200).send(history.map(accel => ({
txid: accel.txid,
added: accel.added,
status: 'completed',
effectiveFee: accel.effective_fee,
effectiveVsize: accel.effective_vsize,
boostRate: accel.boost_rate,
boostCost: accel.boost_cost,
blockHeight: accel.height,
pools: [accel.pool],
})));
}
private async $getAcceleratorAccelerationsHistoryAggregated(req: Request, res: Response): Promise<void> {
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
try {
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
for (const key in response.headers) {
res.setHeader(key, response.headers[key]);
}
response.data.pipe(res);
} catch (e) {
logger.err(`Unable to get aggregated acceleration history from ${url} in $getAcceleratorAccelerationsHistoryAggregated(), ${e}`, this.tag);
res.status(500).end();
}
}
private async $getAcceleratorAccelerationsStats(req: Request, res: Response): Promise<void> {
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
try {
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
for (const key in response.headers) {
res.setHeader(key, response.headers[key]);
}
response.data.pipe(res);
} catch (e) {
logger.err(`Unable to get acceleration stats from ${url} in $getAcceleratorAccelerationsStats(), ${e}`, this.tag);
res.status(500).end();
}
}
}
export default new AccelerationRoutes();

View File

@@ -1,6 +1,6 @@
import logger from '../logger';
import { MempoolTransactionExtended } from '../mempool.interfaces';
import { IEsploraApi } from './bitcoin/esplora-api.interface';
import logger from '../../logger';
import { MempoolTransactionExtended } from '../../mempool.interfaces';
import { IEsploraApi } from '../bitcoin/esplora-api.interface';
const BLOCK_WEIGHT_UNITS = 4_000_000;
const BLOCK_SIGOPS = 80_000;

View File

@@ -1,381 +0,0 @@
import { Application, Request, Response } from 'express';
import config from '../../config';
import { RequiredSpec } from '../../mempool.interfaces';
import bisq from './bisq';
import { MarketsApiError } from './interfaces';
import marketsApi from './markets-api';
class BisqRoutes {
public initRoutes(app: Application) {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/stats', this.getBisqStats)
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/tx/:txId', this.getBisqTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/block/:hash', this.getBisqBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/blocks/tip/height', this.getBisqTip)
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/blocks/:index/:length', this.getBisqBlocks)
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/address/:address', this.getBisqAddress)
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/txs/:index/:length', this.getBisqTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/currencies', this.getBisqMarketCurrencies.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/depth', this.getBisqMarketDepth.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/hloc', this.getBisqMarketHloc.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/markets', this.getBisqMarketMarkets.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/offers', this.getBisqMarketOffers.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/ticker', this.getBisqMarketTicker.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/trades', this.getBisqMarketTrades.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/volumes', this.getBisqMarketVolumes.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/volumes/7d', this.getBisqMarketVolumes7d.bind(this))
;
}
private getBisqStats(req: Request, res: Response) {
const result = bisq.getStats();
res.json(result);
}
private getBisqTip(req: Request, res: Response) {
const result = bisq.getLatestBlockHeight();
res.type('text/plain');
res.send(result.toString());
}
private getBisqTransaction(req: Request, res: Response) {
const result = bisq.getTransaction(req.params.txId);
if (result) {
res.json(result);
} else {
res.status(404).send('Bisq transaction not found');
}
}
private getBisqTransactions(req: Request, res: Response) {
const types: string[] = [];
req.query.types = req.query.types || [];
if (!Array.isArray(req.query.types)) {
res.status(500).send('Types is not an array');
return;
}
for (const _type in req.query.types) {
if (typeof req.query.types[_type] === 'string') {
types.push(req.query.types[_type].toString());
}
}
const index = parseInt(req.params.index, 10) || 0;
const length = parseInt(req.params.length, 10) > 100 ? 100 : parseInt(req.params.length, 10) || 25;
const [transactions, count] = bisq.getTransactions(index, length, types);
res.header('X-Total-Count', count.toString());
res.json(transactions);
}
private getBisqBlock(req: Request, res: Response) {
const result = bisq.getBlock(req.params.hash);
if (result) {
res.json(result);
} else {
res.status(404).send('Bisq block not found');
}
}
private getBisqBlocks(req: Request, res: Response) {
const index = parseInt(req.params.index, 10) || 0;
const length = parseInt(req.params.length, 10) > 100 ? 100 : parseInt(req.params.length, 10) || 25;
const [transactions, count] = bisq.getBlocks(index, length);
res.header('X-Total-Count', count.toString());
res.json(transactions);
}
private getBisqAddress(req: Request, res: Response) {
const result = bisq.getAddress(req.params.address.substr(1));
if (result) {
res.json(result);
} else {
res.status(404).send('Bisq address not found');
}
}
private getBisqMarketCurrencies(req: Request, res: Response) {
const constraints: RequiredSpec = {
'type': {
required: false,
types: ['crypto', 'fiat', 'all']
},
};
const p = this.parseRequestParameters(req.query, constraints);
if (p.error) {
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
return;
}
const result = marketsApi.getCurrencies(p.type);
if (result) {
res.json(result);
} else {
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketCurrencies error'));
}
}
private getBisqMarketDepth(req: Request, res: Response) {
const constraints: RequiredSpec = {
'market': {
required: true,
types: ['@string']
},
};
const p = this.parseRequestParameters(req.query, constraints);
if (p.error) {
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
return;
}
const result = marketsApi.getDepth(p.market);
if (result) {
res.json(result);
} else {
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketDepth error'));
}
}
private getBisqMarketMarkets(req: Request, res: Response) {
const result = marketsApi.getMarkets();
if (result) {
res.json(result);
} else {
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketMarkets error'));
}
}
private getBisqMarketTrades(req: Request, res: Response) {
const constraints: RequiredSpec = {
'market': {
required: true,
types: ['@string']
},
'timestamp_from': {
required: false,
types: ['@number']
},
'timestamp_to': {
required: false,
types: ['@number']
},
'trade_id_to': {
required: false,
types: ['@string']
},
'trade_id_from': {
required: false,
types: ['@string']
},
'direction': {
required: false,
types: ['buy', 'sell']
},
'limit': {
required: false,
types: ['@number']
},
'sort': {
required: false,
types: ['asc', 'desc']
}
};
const p = this.parseRequestParameters(req.query, constraints);
if (p.error) {
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
return;
}
const result = marketsApi.getTrades(p.market, p.timestamp_from,
p.timestamp_to, p.trade_id_from, p.trade_id_to, p.direction, p.limit, p.sort);
if (result) {
res.json(result);
} else {
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketTrades error'));
}
}
private getBisqMarketOffers(req: Request, res: Response) {
const constraints: RequiredSpec = {
'market': {
required: true,
types: ['@string']
},
'direction': {
required: false,
types: ['buy', 'sell']
},
};
const p = this.parseRequestParameters(req.query, constraints);
if (p.error) {
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
return;
}
const result = marketsApi.getOffers(p.market, p.direction);
if (result) {
res.json(result);
} else {
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketOffers error'));
}
}
private getBisqMarketVolumes(req: Request, res: Response) {
const constraints: RequiredSpec = {
'market': {
required: false,
types: ['@string']
},
'interval': {
required: false,
types: ['minute', 'half_hour', 'hour', 'half_day', 'day', 'week', 'month', 'year', 'auto']
},
'timestamp_from': {
required: false,
types: ['@number']
},
'timestamp_to': {
required: false,
types: ['@number']
},
'milliseconds': {
required: false,
types: ['@boolean']
},
'timestamp': {
required: false,
types: ['no', 'yes']
},
};
const p = this.parseRequestParameters(req.query, constraints);
if (p.error) {
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
return;
}
const result = marketsApi.getVolumes(p.market, p.timestamp_from, p.timestamp_to, p.interval, p.milliseconds, p.timestamp);
if (result) {
res.json(result);
} else {
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketVolumes error'));
}
}
private getBisqMarketHloc(req: Request, res: Response) {
const constraints: RequiredSpec = {
'market': {
required: true,
types: ['@string']
},
'interval': {
required: false,
types: ['minute', 'half_hour', 'hour', 'half_day', 'day', 'week', 'month', 'year', 'auto']
},
'timestamp_from': {
required: false,
types: ['@number']
},
'timestamp_to': {
required: false,
types: ['@number']
},
'milliseconds': {
required: false,
types: ['@boolean']
},
'timestamp': {
required: false,
types: ['no', 'yes']
},
};
const p = this.parseRequestParameters(req.query, constraints);
if (p.error) {
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
return;
}
const result = marketsApi.getHloc(p.market, p.interval, p.timestamp_from, p.timestamp_to, p.milliseconds, p.timestamp);
if (result) {
res.json(result);
} else {
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketHloc error'));
}
}
private getBisqMarketTicker(req: Request, res: Response) {
const constraints: RequiredSpec = {
'market': {
required: false,
types: ['@string']
},
};
const p = this.parseRequestParameters(req.query, constraints);
if (p.error) {
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
return;
}
const result = marketsApi.getTicker(p.market);
if (result) {
res.json(result);
} else {
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketTicker error'));
}
}
private getBisqMarketVolumes7d(req: Request, res: Response) {
const result = marketsApi.getVolumesByTime(604800);
if (result) {
res.json(result);
} else {
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketVolumes7d error'));
}
}
private parseRequestParameters(requestParams: object, params: RequiredSpec): { [name: string]: any; } {
const final = {};
for (const i in params) {
if (params.hasOwnProperty(i)) {
if (params[i].required && requestParams[i] === undefined) {
return { error: i + ' parameter missing'};
}
if (typeof requestParams[i] === 'string') {
const str = (requestParams[i] || '').toString().toLowerCase();
if (params[i].types.indexOf('@number') > -1) {
const number = parseInt((str).toString(), 10);
final[i] = number;
} else if (params[i].types.indexOf('@string') > -1) {
final[i] = str;
} else if (params[i].types.indexOf('@boolean') > -1) {
final[i] = str === 'true' || str === 'yes';
} else if (params[i].types.indexOf(str) > -1) {
final[i] = str;
} else {
return { error: i + ' parameter invalid'};
}
} else if (typeof requestParams[i] === 'number') {
final[i] = requestParams[i];
}
}
}
return final;
}
private getBisqMarketErrorResponse(message: string): MarketsApiError {
return {
'success': 0,
'error': message
};
}
}
export default new BisqRoutes;

View File

@@ -1,359 +0,0 @@
import config from '../../config';
import * as fs from 'fs';
import axios, { AxiosResponse } from 'axios';
import * as http from 'http';
import * as https from 'https';
import { SocksProxyAgent } from 'socks-proxy-agent';
import { BisqBlocks, BisqBlock, BisqTransaction, BisqStats, BisqTrade } from './interfaces';
import { Common } from '../common';
import { BlockExtended } from '../../mempool.interfaces';
import backendInfo from '../backend-info';
import logger from '../../logger';
class Bisq {
private static BLOCKS_JSON_FILE_PATH = config.BISQ.DATA_PATH + '/json/all/blocks.json';
private latestBlockHeight = 0;
private blocks: BisqBlock[] = [];
private allBlocks: BisqBlock[] = [];
private transactions: BisqTransaction[] = [];
private transactionIndex: { [txId: string]: BisqTransaction } = {};
private blockIndex: { [hash: string]: BisqBlock } = {};
private addressIndex: { [address: string]: BisqTransaction[] } = {};
private stats: BisqStats = {
minted: 0,
burnt: 0,
addresses: 0,
unspent_txos: 0,
spent_txos: 0,
};
private price: number = 0;
private priceUpdateCallbackFunction: ((price: number) => void) | undefined;
private topDirectoryWatcher: fs.FSWatcher | undefined;
private subdirectoryWatcher: fs.FSWatcher | undefined;
constructor() {}
startBisqService(): void {
try {
this.checkForBisqDataFolder();
} catch (e) {
logger.info('Retrying to start bisq service in 3 minutes');
setTimeout(this.startBisqService.bind(this), 180000);
return;
}
this.loadBisqDumpFile();
setInterval(this.updatePrice.bind(this), 1000 * 60 * 60);
this.updatePrice();
this.startTopDirectoryWatcher();
this.startSubDirectoryWatcher();
}
handleNewBitcoinBlock(block: BlockExtended): void {
if (block.height - 10 > this.latestBlockHeight && this.latestBlockHeight !== 0) {
logger.warn(`Bitcoin block height (#${block.height}) has diverged from the latest Bisq block height (#${this.latestBlockHeight}). Restarting watchers...`);
this.startTopDirectoryWatcher();
this.startSubDirectoryWatcher();
}
}
getTransaction(txId: string): BisqTransaction | undefined {
return this.transactionIndex[txId];
}
getTransactions(start: number, length: number, types: string[]): [BisqTransaction[], number] {
let transactions = this.transactions;
if (types.length) {
transactions = transactions.filter((tx) => types.indexOf(tx.txType) > -1);
}
return [transactions.slice(start, length + start), transactions.length];
}
getBlock(hash: string): BisqBlock | undefined {
return this.blockIndex[hash];
}
getAddress(hash: string): BisqTransaction[] {
return this.addressIndex[hash];
}
getBlocks(start: number, length: number): [BisqBlock[], number] {
return [this.blocks.slice(start, length + start), this.blocks.length];
}
getStats(): BisqStats {
return this.stats;
}
setPriceCallbackFunction(fn: (price: number) => void) {
this.priceUpdateCallbackFunction = fn;
}
getLatestBlockHeight(): number {
return this.latestBlockHeight;
}
private checkForBisqDataFolder() {
if (!fs.existsSync(Bisq.BLOCKS_JSON_FILE_PATH)) {
logger.warn(Bisq.BLOCKS_JSON_FILE_PATH + ` doesn't exist. Make sure Bisq is running and the config is correct before starting the server.`);
throw new Error(`Cannot load BISQ ${Bisq.BLOCKS_JSON_FILE_PATH} file`);
}
}
private startTopDirectoryWatcher() {
if (this.topDirectoryWatcher) {
this.topDirectoryWatcher.close();
}
let fsWait: NodeJS.Timeout | null = null;
this.topDirectoryWatcher = fs.watch(config.BISQ.DATA_PATH + '/json', () => {
if (fsWait) {
clearTimeout(fsWait);
}
if (this.subdirectoryWatcher) {
this.subdirectoryWatcher.close();
}
fsWait = setTimeout(() => {
logger.debug(`Bisq restart detected. Resetting both watchers in 3 minutes.`);
setTimeout(() => {
this.startTopDirectoryWatcher();
this.startSubDirectoryWatcher();
this.loadBisqDumpFile();
}, 180000);
}, 15000);
});
}
private startSubDirectoryWatcher() {
if (this.subdirectoryWatcher) {
this.subdirectoryWatcher.close();
}
if (!fs.existsSync(Bisq.BLOCKS_JSON_FILE_PATH)) {
logger.warn(Bisq.BLOCKS_JSON_FILE_PATH + ` doesn't exist. Trying to restart sub directory watcher again in 3 minutes.`);
setTimeout(() => this.startSubDirectoryWatcher(), 180000);
return;
}
let fsWait: NodeJS.Timeout | null = null;
this.subdirectoryWatcher = fs.watch(config.BISQ.DATA_PATH + '/json/all', () => {
if (fsWait) {
clearTimeout(fsWait);
}
fsWait = setTimeout(() => {
logger.debug(`Change detected in the Bisq data folder.`);
this.loadBisqDumpFile();
}, 2000);
});
}
private async updatePrice() {
type axiosOptions = {
headers: {
'User-Agent': string
};
timeout: number;
httpAgent?: http.Agent;
httpsAgent?: https.Agent;
}
const setDelay = (secs: number = 1): Promise<void> => new Promise(resolve => setTimeout(() => resolve(), secs * 1000));
const BISQ_URL = (config.SOCKS5PROXY.ENABLED === true) && (config.SOCKS5PROXY.USE_ONION === true) ? config.EXTERNAL_DATA_SERVER.BISQ_ONION : config.EXTERNAL_DATA_SERVER.BISQ_URL;
const isHTTP = (new URL(BISQ_URL).protocol.split(':')[0] === 'http') ? true : false;
const axiosOptions: axiosOptions = {
headers: {
'User-Agent': (config.MEMPOOL.USER_AGENT === 'mempool') ? `mempool/v${backendInfo.getBackendInfo().version}` : `${config.MEMPOOL.USER_AGENT}`
},
timeout: config.SOCKS5PROXY.ENABLED ? 30000 : 10000
};
let retry = 0;
while(retry < config.MEMPOOL.EXTERNAL_MAX_RETRY) {
try {
if (config.SOCKS5PROXY.ENABLED) {
const socksOptions: any = {
agentOptions: {
keepAlive: true,
},
hostname: config.SOCKS5PROXY.HOST,
port: config.SOCKS5PROXY.PORT
};
if (config.SOCKS5PROXY.USERNAME && config.SOCKS5PROXY.PASSWORD) {
socksOptions.username = config.SOCKS5PROXY.USERNAME;
socksOptions.password = config.SOCKS5PROXY.PASSWORD;
} else {
// Retry with different tor circuits https://stackoverflow.com/a/64960234
socksOptions.username = `circuit${retry}`;
}
// Handle proxy agent for onion addresses
if (isHTTP) {
axiosOptions.httpAgent = new SocksProxyAgent(socksOptions);
} else {
axiosOptions.httpsAgent = new SocksProxyAgent(socksOptions);
}
}
const data: AxiosResponse = await axios.get(`${BISQ_URL}/trades/?market=bsq_btc`, axiosOptions);
if (data.statusText === 'error' || !data.data) {
throw new Error(`Could not fetch data from Bisq market, Error: ${data.status}`);
}
const prices: number[] = [];
data.data.forEach((trade) => {
prices.push(parseFloat(trade.price) * 100000000);
});
prices.sort((a, b) => a - b);
this.price = Common.median(prices);
if (this.priceUpdateCallbackFunction) {
this.priceUpdateCallbackFunction(this.price);
}
logger.debug('Successfully updated Bisq market price');
break;
} catch (e) {
logger.err('Error updating Bisq market price: ' + (e instanceof Error ? e.message : e));
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL);
retry++;
}
}
}
private async loadBisqDumpFile(): Promise<void> {
this.allBlocks = [];
try {
await this.loadData();
this.buildIndex();
this.calculateStats();
} catch (e) {
logger.info('Cannot load bisq dump file because: ' + (e instanceof Error ? e.message : e));
}
}
private buildIndex() {
const start = new Date().getTime();
this.transactions = [];
this.transactionIndex = {};
this.addressIndex = {};
this.allBlocks.forEach((block) => {
/* Build block index */
if (!this.blockIndex[block.hash]) {
this.blockIndex[block.hash] = block;
}
/* Build transactions index */
block.txs.forEach((tx) => {
this.transactions.push(tx);
this.transactionIndex[tx.id] = tx;
});
});
/* Build address index */
this.transactions.forEach((tx) => {
tx.inputs.forEach((input) => {
if (!this.addressIndex[input.address]) {
this.addressIndex[input.address] = [];
}
if (this.addressIndex[input.address].indexOf(tx) === -1) {
this.addressIndex[input.address].push(tx);
}
});
tx.outputs.forEach((output) => {
if (!this.addressIndex[output.address]) {
this.addressIndex[output.address] = [];
}
if (this.addressIndex[output.address].indexOf(tx) === -1) {
this.addressIndex[output.address].push(tx);
}
});
});
const time = new Date().getTime() - start;
logger.debug('Bisq data index rebuilt in ' + time + ' ms');
}
private calculateStats() {
let minted = 0;
let burned = 0;
let unspent = 0;
let spent = 0;
this.transactions.forEach((tx) => {
tx.outputs.forEach((output) => {
if (output.opReturn) {
return;
}
if (output.txOutputType === 'GENESIS_OUTPUT' || output.txOutputType === 'ISSUANCE_CANDIDATE_OUTPUT' && output.isVerified) {
minted += output.bsqAmount;
}
if (output.isUnspent) {
unspent++;
} else {
spent++;
}
});
burned += tx['burntFee'];
});
this.stats = {
addresses: Object.keys(this.addressIndex).length,
minted: minted / 100,
burnt: burned / 100,
spent_txos: spent,
unspent_txos: unspent,
};
}
private async loadData(): Promise<any> {
if (!fs.existsSync(Bisq.BLOCKS_JSON_FILE_PATH)) {
throw new Error(Bisq.BLOCKS_JSON_FILE_PATH + ` doesn't exist`);
}
const readline = require('readline');
const events = require('events');
const rl = readline.createInterface({
input: fs.createReadStream(Bisq.BLOCKS_JSON_FILE_PATH),
crlfDelay: Infinity
});
let blockBuffer = '';
let readingBlock = false;
let lineCount = 1;
const start = new Date().getTime();
logger.debug('Processing Bisq data dump...');
rl.on('line', (line) => {
if (lineCount === 2) {
line = line.replace(' "chainHeight": ', '');
this.latestBlockHeight = parseInt(line, 10);
}
if (line === ' {') {
readingBlock = true;
} else if (line === ' },') {
blockBuffer += '}';
try {
const block: BisqBlock = JSON.parse(blockBuffer);
this.allBlocks.push(block);
readingBlock = false;
blockBuffer = '';
} catch (e) {
logger.debug(blockBuffer);
throw Error(`Unable to parse Bisq data dump at line ${lineCount}` + (e instanceof Error ? e.message : e));
}
}
if (readingBlock === true) {
blockBuffer += line;
}
++lineCount;
});
await events.once(rl, 'close');
this.allBlocks.reverse();
this.blocks = this.allBlocks.filter((block) => block.txs.length > 0);
const time = new Date().getTime() - start;
logger.debug('Bisq dump processed in ' + time + ' ms');
}
}
export default new Bisq();

View File

@@ -1,258 +0,0 @@
export interface BisqBlocks {
chainHeight: number;
blocks: BisqBlock[];
}
export interface BisqBlock {
height: number;
time: number;
hash: string;
previousBlockHash: string;
txs: BisqTransaction[];
}
export interface BisqTransaction {
txVersion: string;
id: string;
blockHeight: number;
blockHash: string;
time: number;
inputs: BisqInput[];
outputs: BisqOutput[];
txType: string;
txTypeDisplayString: string;
burntFee: number;
invalidatedBsq: number;
unlockBlockHeight: number;
}
export interface BisqStats {
minted: number;
burnt: number;
addresses: number;
unspent_txos: number;
spent_txos: number;
}
interface BisqInput {
spendingTxOutputIndex: number;
spendingTxId: string;
bsqAmount: number;
isVerified: boolean;
address: string;
time: number;
}
interface BisqOutput {
txVersion: string;
txId: string;
index: number;
bsqAmount: number;
btcAmount: number;
height: number;
isVerified: boolean;
burntFee: number;
invalidatedBsq: number;
address: string;
scriptPubKey: BisqScriptPubKey;
time: any;
txType: string;
txTypeDisplayString: string;
txOutputType: string;
txOutputTypeDisplayString: string;
lockTime: number;
isUnspent: boolean;
spentInfo: SpentInfo;
opReturn?: string;
}
interface BisqScriptPubKey {
addresses: string[];
asm: string;
hex: string;
reqSigs?: number;
type: string;
}
interface SpentInfo {
height: number;
inputIndex: number;
txId: string;
}
export interface BisqTrade {
direction: string;
price: string;
amount: string;
volume: string;
payment_method: string;
trade_id: string;
trade_date: number;
market?: string;
}
export interface Currencies { [txid: string]: Currency; }
export interface Currency {
code: string;
name: string;
precision: number;
_type: string;
}
export interface Depth { [market: string]: Market; }
interface Market {
'buys': string[];
'sells': string[];
}
export interface HighLowOpenClose {
period_start: number | string;
open: string;
high: string;
low: string;
close: string;
volume_left: string;
volume_right: string;
avg: string;
}
export interface Markets { [txid: string]: Pair; }
interface Pair {
pair: string;
lname: string;
rname: string;
lsymbol: string;
rsymbol: string;
lprecision: number;
rprecision: number;
ltype: string;
rtype: string;
name: string;
}
export interface Offers { [market: string]: OffersMarket; }
interface OffersMarket {
buys: Offer[] | null;
sells: Offer[] | null;
}
export interface OffersData {
direction: string;
currencyCode: string;
minAmount: number;
amount: number;
price: number;
date: number;
useMarketBasedPrice: boolean;
marketPriceMargin: number;
paymentMethod: string;
id: string;
currencyPair: string;
primaryMarketDirection: string;
priceDisplayString: string;
primaryMarketAmountDisplayString: string;
primaryMarketMinAmountDisplayString: string;
primaryMarketVolumeDisplayString: string;
primaryMarketMinVolumeDisplayString: string;
primaryMarketPrice: number;
primaryMarketAmount: number;
primaryMarketMinAmount: number;
primaryMarketVolume: number;
primaryMarketMinVolume: number;
}
export interface Offer {
offer_id: string;
offer_date: number;
direction: string;
min_amount: string;
amount: string;
price: string;
volume: string;
payment_method: string;
offer_fee_txid: any;
}
export interface Tickers { [market: string]: Ticker | null; }
export interface Ticker {
last: string;
high: string;
low: string;
volume_left: string;
volume_right: string;
buy: string | null;
sell: string | null;
}
export interface Trade {
direction: string;
price: string;
amount: string;
volume: string;
payment_method: string;
trade_id: string;
trade_date: number;
}
export interface TradesData {
currency: string;
direction: string;
tradePrice: number;
tradeAmount: number;
tradeDate: number;
paymentMethod: string;
offerDate: number;
useMarketBasedPrice: boolean;
marketPriceMargin: number;
offerAmount: number;
offerMinAmount: number;
offerId: string;
depositTxId?: string;
currencyPair: string;
primaryMarketDirection: string;
primaryMarketTradePrice: number;
primaryMarketTradeAmount: number;
primaryMarketTradeVolume: number;
_market: string;
_tradePriceStr: string;
_tradeAmountStr: string;
_tradeVolumeStr: string;
_offerAmountStr: string;
_tradePrice: number;
_tradeAmount: number;
_tradeVolume: number;
_offerAmount: number;
}
export interface MarketVolume {
period_start: number;
num_trades: number;
volume: string;
}
export interface MarketsApiError {
success: number;
error: string;
}
export type Interval = 'minute' | 'half_hour' | 'hour' | 'half_day' | 'day' | 'week' | 'month' | 'year' | 'auto';
export interface SummarizedIntervals { [market: string]: SummarizedInterval; }
export interface SummarizedInterval {
'period_start': number;
'open': number;
'close': number;
'high': number;
'low': number;
'avg': number;
'volume_right': number;
'volume_left': number;
}

View File

@@ -1,679 +0,0 @@
import { Currencies, OffersData, TradesData, Depth, Currency, Interval, HighLowOpenClose,
Markets, Offers, Offer, BisqTrade, MarketVolume, Tickers, Ticker, SummarizedIntervals, SummarizedInterval } from './interfaces';
const strtotime = require('./strtotime');
class BisqMarketsApi {
private cryptoCurrencyData: Currency[] = [];
private fiatCurrencyData: Currency[] = [];
private activeCryptoCurrencyData: Currency[] = [];
private activeFiatCurrencyData: Currency[] = [];
private offersData: OffersData[] = [];
private tradesData: TradesData[] = [];
private fiatCurrenciesIndexed: { [code: string]: true } = {};
private allCurrenciesIndexed: { [code: string]: Currency } = {};
private tradeDataByMarket: { [market: string]: TradesData[] } = {};
private tickersCache: Ticker | Tickers | null = null;
constructor() { }
setOffersData(offers: OffersData[]) {
this.offersData = offers;
}
setTradesData(trades: TradesData[]) {
this.tradesData = trades;
this.tradeDataByMarket = {};
this.tradesData.forEach((trade) => {
trade._market = trade.currencyPair.toLowerCase().replace('/', '_');
if (!this.tradeDataByMarket[trade._market]) {
this.tradeDataByMarket[trade._market] = [];
}
this.tradeDataByMarket[trade._market].push(trade);
});
}
setCurrencyData(cryptoCurrency: Currency[], fiatCurrency: Currency[], activeCryptoCurrency: Currency[], activeFiatCurrency: Currency[]) {
this.cryptoCurrencyData = cryptoCurrency,
this.fiatCurrencyData = fiatCurrency,
this.activeCryptoCurrencyData = activeCryptoCurrency,
this.activeFiatCurrencyData = activeFiatCurrency;
this.fiatCurrenciesIndexed = {};
this.allCurrenciesIndexed = {};
this.fiatCurrencyData.forEach((currency) => {
currency._type = 'fiat';
this.fiatCurrenciesIndexed[currency.code] = true;
this.allCurrenciesIndexed[currency.code] = currency;
});
this.cryptoCurrencyData.forEach((currency) => {
currency._type = 'crypto';
this.allCurrenciesIndexed[currency.code] = currency;
});
}
updateCache() {
this.tickersCache = null;
this.tickersCache = this.getTicker();
}
getCurrencies(
type: 'crypto' | 'fiat' | 'active' | 'all' = 'all',
): Currencies {
let currencies: Currency[];
switch (type) {
case 'fiat':
currencies = this.fiatCurrencyData;
break;
case 'crypto':
currencies = this.cryptoCurrencyData;
break;
case 'active':
currencies = this.activeCryptoCurrencyData.concat(this.activeFiatCurrencyData);
break;
case 'all':
default:
currencies = this.cryptoCurrencyData.concat(this.fiatCurrencyData);
}
const result = {};
currencies.forEach((currency) => {
result[currency.code] = currency;
});
return result;
}
getDepth(
market: string,
): Depth {
const currencyPair = market.replace('_', '/').toUpperCase();
const buys = this.offersData
.filter((offer) => offer.currencyPair === currencyPair && offer.primaryMarketDirection === 'BUY')
.map((offer) => offer.price)
.sort((a, b) => b - a)
.map((price) => this.intToBtc(price));
const sells = this.offersData
.filter((offer) => offer.currencyPair === currencyPair && offer.primaryMarketDirection === 'SELL')
.map((offer) => offer.price)
.sort((a, b) => a - b)
.map((price) => this.intToBtc(price));
const result = {};
result[market] = {
'buys': buys,
'sells': sells,
};
return result;
}
getOffers(
market: string,
direction?: 'buy' | 'sell',
): Offers {
const currencyPair = market.replace('_', '/').toUpperCase();
let buys: Offer[] | null = null;
let sells: Offer[] | null = null;
if (!direction || direction === 'buy') {
buys = this.offersData
.filter((offer) => offer.currencyPair === currencyPair && offer.primaryMarketDirection === 'BUY')
.sort((a, b) => b.price - a.price)
.map((offer) => this.offerDataToOffer(offer, market));
}
if (!direction || direction === 'sell') {
sells = this.offersData
.filter((offer) => offer.currencyPair === currencyPair && offer.primaryMarketDirection === 'SELL')
.sort((a, b) => a.price - b.price)
.map((offer) => this.offerDataToOffer(offer, market));
}
const result: Offers = {};
result[market] = {
'buys': buys,
'sells': sells,
};
return result;
}
getMarkets(): Markets {
const allCurrencies = this.getCurrencies();
const activeCurrencies = this.getCurrencies('active');
const markets = {};
for (const currency of Object.keys(activeCurrencies)) {
if (allCurrencies[currency].code === 'BTC') {
continue;
}
const isFiat = allCurrencies[currency]._type === 'fiat';
const pmarketname = allCurrencies['BTC']['name'];
const lsymbol = isFiat ? 'BTC' : currency;
const rsymbol = isFiat ? currency : 'BTC';
const lname = isFiat ? pmarketname : allCurrencies[currency].name;
const rname = isFiat ? allCurrencies[currency].name : pmarketname;
const ltype = isFiat ? 'crypto' : allCurrencies[currency]._type;
const rtype = isFiat ? 'fiat' : 'crypto';
const lprecision = 8;
const rprecision = isFiat ? 2 : 8;
const pair = lsymbol.toLowerCase() + '_' + rsymbol.toLowerCase();
markets[pair] = {
'pair': pair,
'lname': lname,
'rname': rname,
'lsymbol': lsymbol,
'rsymbol': rsymbol,
'lprecision': lprecision,
'rprecision': rprecision,
'ltype': ltype,
'rtype': rtype,
'name': lname + '/' + rname,
};
}
return markets;
}
getTrades(
market: string,
timestamp_from?: number,
timestamp_to?: number,
trade_id_from?: string,
trade_id_to?: string,
direction?: 'buy' | 'sell',
limit: number = 100,
sort: 'asc' | 'desc' = 'desc',
): BisqTrade[] {
limit = Math.min(limit, 2000);
const _market = market === 'all' ? undefined : market;
if (!timestamp_from) {
timestamp_from = new Date('2016-01-01').getTime() / 1000;
}
if (!timestamp_to) {
timestamp_to = new Date().getTime() / 1000;
}
const matches = this.getTradesByCriteria(_market, timestamp_to, timestamp_from,
trade_id_to, trade_id_from, direction, sort, limit, false);
if (sort === 'asc') {
matches.sort((a, b) => a.tradeDate - b.tradeDate);
} else {
matches.sort((a, b) => b.tradeDate - a.tradeDate);
}
return matches.map((trade) => {
const bsqTrade: BisqTrade = {
direction: trade.primaryMarketDirection,
price: trade._tradePriceStr,
amount: trade._tradeAmountStr,
volume: trade._tradeVolumeStr,
payment_method: trade.paymentMethod,
trade_id: trade.offerId,
trade_date: trade.tradeDate,
};
if (market === 'all') {
bsqTrade.market = trade._market;
}
return bsqTrade;
});
}
getVolumes(
market?: string,
timestamp_from?: number,
timestamp_to?: number,
interval: Interval = 'auto',
milliseconds?: boolean,
timestamp: 'no' | 'yes' = 'yes',
): MarketVolume[] {
if (milliseconds) {
timestamp_from = timestamp_from ? timestamp_from / 1000 : timestamp_from;
timestamp_to = timestamp_to ? timestamp_to / 1000 : timestamp_to;
}
if (!timestamp_from) {
timestamp_from = new Date('2016-01-01').getTime() / 1000;
}
if (!timestamp_to) {
timestamp_to = new Date().getTime() / 1000;
}
const trades = this.getTradesByCriteria(market, timestamp_to, timestamp_from,
undefined, undefined, undefined, 'asc', Number.MAX_SAFE_INTEGER);
if (interval === 'auto') {
const range = timestamp_to - timestamp_from;
interval = this.getIntervalFromRange(range);
}
const intervals: any = {};
const marketVolumes: MarketVolume[] = [];
for (const trade of trades) {
const traded_at = trade['tradeDate'] / 1000;
const interval_start = this.intervalStart(traded_at, interval);
if (!intervals[interval_start]) {
intervals[interval_start] = {
'volume': 0,
'num_trades': 0,
};
}
const period = intervals[interval_start];
period['period_start'] = interval_start;
period['volume'] += this.fiatCurrenciesIndexed[trade.currency] ? trade._tradeAmount : trade._tradeVolume;
period['num_trades']++;
}
for (const p in intervals) {
if (intervals.hasOwnProperty(p)) {
const period = intervals[p];
marketVolumes.push({
period_start: timestamp === 'no' ? new Date(period['period_start'] * 1000).toISOString() : period['period_start'],
num_trades: period['num_trades'],
volume: this.intToBtc(period['volume']),
});
}
}
return marketVolumes;
}
getTicker(
market?: string,
): Tickers | Ticker | null {
if (market) {
return this.getTickerFromMarket(market);
}
if (this.tickersCache) {
return this.tickersCache;
}
const allMarkets = this.getMarkets();
const tickers = {};
for (const m in allMarkets) {
if (allMarkets.hasOwnProperty(m)) {
tickers[allMarkets[m].pair] = this.getTickerFromMarket(allMarkets[m].pair);
}
}
return tickers;
}
getTickerFromMarket(market: string): Ticker | null {
let ticker: Ticker;
const timestamp_from = strtotime('-24 hour');
const timestamp_to = new Date().getTime() / 1000;
const trades = this.getTradesByCriteria(market, timestamp_to, timestamp_from,
undefined, undefined, undefined, 'asc', Number.MAX_SAFE_INTEGER);
const periods: SummarizedInterval[] = Object.values(this.getTradesSummarized(trades, timestamp_from));
const allCurrencies = this.getCurrencies();
const currencyRight = allCurrencies[market.split('_')[1].toUpperCase()];
if (periods[0]) {
ticker = {
'last': this.intToBtc(periods[0].close),
'high': this.intToBtc(periods[0].high),
'low': this.intToBtc(periods[0].low),
'volume_left': this.intToBtc(periods[0].volume_left),
'volume_right': this.intToBtc(periods[0].volume_right),
'buy': null,
'sell': null,
};
} else {
const lastTrade = this.tradeDataByMarket[market];
if (!lastTrade) {
return null;
}
const tradePrice = lastTrade[0].primaryMarketTradePrice * Math.pow(10, 8 - currencyRight.precision);
const lastTradePrice = this.intToBtc(tradePrice);
ticker = {
'last': lastTradePrice,
'high': lastTradePrice,
'low': lastTradePrice,
'volume_left': '0',
'volume_right': '0',
'buy': null,
'sell': null,
};
}
const timestampFromMilli = timestamp_from * 1000;
const timestampToMilli = timestamp_to * 1000;
const currencyPair = market.replace('_', '/').toUpperCase();
const offersData = this.offersData.slice().sort((a, b) => a.price - b.price);
const buy = offersData.find((offer) => offer.currencyPair === currencyPair
&& offer.primaryMarketDirection === 'BUY'
&& offer.date >= timestampFromMilli
&& offer.date <= timestampToMilli
);
const sell = offersData.find((offer) => offer.currencyPair === currencyPair
&& offer.primaryMarketDirection === 'SELL'
&& offer.date >= timestampFromMilli
&& offer.date <= timestampToMilli
);
if (buy) {
ticker.buy = this.intToBtc(buy.primaryMarketPrice * Math.pow(10, 8 - currencyRight.precision));
}
if (sell) {
ticker.sell = this.intToBtc(sell.primaryMarketPrice * Math.pow(10, 8 - currencyRight.precision));
}
return ticker;
}
getHloc(
market: string,
interval: Interval = 'auto',
timestamp_from?: number,
timestamp_to?: number,
milliseconds?: boolean,
timestamp: 'no' | 'yes' = 'yes',
): HighLowOpenClose[] {
if (milliseconds) {
timestamp_from = timestamp_from ? timestamp_from / 1000 : timestamp_from;
timestamp_to = timestamp_to ? timestamp_to / 1000 : timestamp_to;
}
if (!timestamp_from) {
timestamp_from = new Date('2016-01-01').getTime() / 1000;
}
if (!timestamp_to) {
timestamp_to = new Date().getTime() / 1000;
}
const trades = this.getTradesByCriteria(market, timestamp_to, timestamp_from,
undefined, undefined, undefined, 'asc', Number.MAX_SAFE_INTEGER);
if (interval === 'auto') {
const range = timestamp_to - timestamp_from;
interval = this.getIntervalFromRange(range);
}
const intervals = this.getTradesSummarized(trades, timestamp_from, interval);
const hloc: HighLowOpenClose[] = [];
for (const p in intervals) {
if (intervals.hasOwnProperty(p)) {
const period = intervals[p];
hloc.push({
period_start: timestamp === 'no' ? new Date(period['period_start'] * 1000).toISOString() : period['period_start'],
open: this.intToBtc(period['open']),
close: this.intToBtc(period['close']),
high: this.intToBtc(period['high']),
low: this.intToBtc(period['low']),
avg: this.intToBtc(period['avg']),
volume_right: this.intToBtc(period['volume_right']),
volume_left: this.intToBtc(period['volume_left']),
});
}
}
return hloc;
}
private getIntervalFromRange(range: number): Interval {
// two days range loads minute data
if (range <= 3600) {
// up to one hour range loads minutely data
return 'minute';
} else if (range <= 1 * 24 * 3600) {
// up to one day range loads half-hourly data
return 'half_hour';
} else if (range <= 3 * 24 * 3600) {
// up to 3 day range loads hourly data
return 'hour';
} else if (range <= 7 * 24 * 3600) {
// up to 7 day range loads half-daily data
return 'half_day';
} else if (range <= 60 * 24 * 3600) {
// up to 2 month range loads daily data
return 'day';
} else if (range <= 12 * 31 * 24 * 3600) {
// up to one year range loads weekly data
return 'week';
} else if (range <= 12 * 31 * 24 * 3600) {
// up to 5 year range loads monthly data
return 'month';
} else {
// greater range loads yearly data
return 'year';
}
}
getVolumesByTime(time: number): MarketVolume[] {
const timestamp_from = new Date().getTime() / 1000 - time;
const timestamp_to = new Date().getTime() / 1000;
const trades = this.getTradesByCriteria(undefined, timestamp_to, timestamp_from,
undefined, undefined, undefined, 'asc', Number.MAX_SAFE_INTEGER);
const markets: any = {};
for (const trade of trades) {
if (!markets[trade._market]) {
markets[trade._market] = {
'volume': 0,
'num_trades': 0,
};
}
markets[trade._market]['volume'] += this.fiatCurrenciesIndexed[trade.currency] ? trade._tradeAmount : trade._tradeVolume;
markets[trade._market]['num_trades']++;
}
return markets;
}
private getTradesSummarized(trades: TradesData[], timestamp_from: number, interval?: string): SummarizedIntervals {
const intervals: any = {};
const intervals_prices: any = {};
for (const trade of trades) {
const traded_at = trade.tradeDate / 1000;
const interval_start = !interval ? timestamp_from : this.intervalStart(traded_at, interval);
if (!intervals[interval_start]) {
intervals[interval_start] = {
'open': 0,
'close': 0,
'high': 0,
'low': 0,
'avg': 0,
'volume_right': 0,
'volume_left': 0,
};
intervals_prices[interval_start] = [];
}
const period = intervals[interval_start];
const price = trade._tradePrice;
if (!intervals_prices[interval_start]['leftvol']) {
intervals_prices[interval_start]['leftvol'] = [];
}
if (!intervals_prices[interval_start]['rightvol']) {
intervals_prices[interval_start]['rightvol'] = [];
}
intervals_prices[interval_start]['leftvol'].push(trade._tradeAmount);
intervals_prices[interval_start]['rightvol'].push(trade._tradeVolume);
if (price) {
const plow = period['low'];
period['period_start'] = interval_start;
period['open'] = period['open'] || price;
period['close'] = price;
period['high'] = price > period['high'] ? price : period['high'];
period['low'] = (plow && price > plow) ? period['low'] : price;
period['avg'] = intervals_prices[interval_start]['rightvol'].reduce((p: number, c: number) => c + p, 0)
/ intervals_prices[interval_start]['leftvol'].reduce((c: number, p: number) => c + p, 0) * 100000000;
period['volume_left'] += trade._tradeAmount;
period['volume_right'] += trade._tradeVolume;
}
}
return intervals;
}
private getTradesByCriteria(
market: string | undefined,
timestamp_to: number,
timestamp_from: number,
trade_id_to: string | undefined,
trade_id_from: string | undefined,
direction: 'buy' | 'sell' | undefined,
sort: string,
limit: number,
integerAmounts: boolean = true,
): TradesData[] {
let trade_id_from_ts: number | null = null;
let trade_id_to_ts: number | null = null;
const allCurrencies = this.getCurrencies();
const timestampFromMilli = timestamp_from * 1000;
const timestampToMilli = timestamp_to * 1000;
// note: the offer_id_from/to depends on iterating over trades in
// descending chronological order.
const tradesDataSorted = this.tradesData.slice();
if (sort === 'asc') {
tradesDataSorted.reverse();
}
let matches: TradesData[] = [];
for (const trade of tradesDataSorted) {
if (trade_id_from === trade.offerId) {
trade_id_from_ts = trade.tradeDate;
}
if (trade_id_to === trade.offerId) {
trade_id_to_ts = trade.tradeDate;
}
if (trade_id_to && trade_id_to_ts === null) {
continue;
}
if (trade_id_from && trade_id_from_ts != null && trade_id_from_ts !== trade.tradeDate) {
continue;
}
if (market && market !== trade._market) {
continue;
}
if (timestampFromMilli && timestampFromMilli > trade.tradeDate) {
continue;
}
if (timestampToMilli && timestampToMilli < trade.tradeDate) {
continue;
}
if (direction && direction !== trade.direction.toLowerCase()) {
continue;
}
// Filter out bogus trades with BTC/BTC or XXX/XXX market.
// See github issue: https://github.com/bitsquare/bitsquare/issues/883
const currencyPairs = trade.currencyPair.split('/');
if (currencyPairs[0] === currencyPairs[1]) {
continue;
}
const currencyLeft = allCurrencies[currencyPairs[0]];
const currencyRight = allCurrencies[currencyPairs[1]];
if (!currencyLeft || !currencyRight) {
continue;
}
const tradePrice = trade.primaryMarketTradePrice * Math.pow(10, 8 - currencyRight.precision);
const tradeAmount = trade.primaryMarketTradeAmount * Math.pow(10, 8 - currencyLeft.precision);
const tradeVolume = trade.primaryMarketTradeVolume * Math.pow(10, 8 - currencyRight.precision);
if (integerAmounts) {
trade._tradePrice = tradePrice;
trade._tradeAmount = tradeAmount;
trade._tradeVolume = tradeVolume;
trade._offerAmount = trade.offerAmount;
} else {
trade._tradePriceStr = this.intToBtc(tradePrice);
trade._tradeAmountStr = this.intToBtc(tradeAmount);
trade._tradeVolumeStr = this.intToBtc(tradeVolume);
trade._offerAmountStr = this.intToBtc(trade.offerAmount);
}
matches.push(trade);
if (matches.length >= limit) {
break;
}
}
if ((trade_id_from && !trade_id_from_ts) || (trade_id_to && !trade_id_to_ts)) {
matches = [];
}
return matches;
}
private intervalStart(ts: number, interval: string): number {
switch (interval) {
case 'minute':
return (ts - (ts % 60));
case '10_minute':
return (ts - (ts % 600));
case 'half_hour':
return (ts - (ts % 1800));
case 'hour':
return (ts - (ts % 3600));
case 'half_day':
return (ts - (ts % (3600 * 12)));
case 'day':
return strtotime('midnight today', ts);
case 'week':
return strtotime('midnight sunday last week', ts);
case 'month':
return strtotime('midnight first day of this month', ts);
case 'year':
return strtotime('midnight first day of january', ts);
default:
throw new Error('Unsupported interval');
}
}
private offerDataToOffer(offer: OffersData, market: string): Offer {
const currencyPairs = market.split('_');
const currencyRight = this.allCurrenciesIndexed[currencyPairs[1].toUpperCase()];
const currencyLeft = this.allCurrenciesIndexed[currencyPairs[0].toUpperCase()];
const price = offer['primaryMarketPrice'] * Math.pow( 10, 8 - currencyRight['precision']);
const amount = offer['primaryMarketAmount'] * Math.pow( 10, 8 - currencyLeft['precision']);
const volume = offer['primaryMarketVolume'] * Math.pow( 10, 8 - currencyRight['precision']);
return {
offer_id: offer.id,
offer_date: offer.date,
direction: offer.primaryMarketDirection,
min_amount: this.intToBtc(offer.minAmount),
amount: this.intToBtc(amount),
price: this.intToBtc(price),
volume: this.intToBtc(volume),
payment_method: offer.paymentMethod,
offer_fee_txid: null,
};
}
private intToBtc(val: number): string {
return (val / 100000000).toFixed(8);
}
}
export default new BisqMarketsApi();

View File

@@ -1,137 +0,0 @@
import config from '../../config';
import * as fs from 'fs';
import { OffersData as OffersData, TradesData, Currency } from './interfaces';
import bisqMarket from './markets-api';
import logger from '../../logger';
class Bisq {
private static FOLDER_WATCH_CHANGE_DETECTION_DEBOUNCE = 4000;
private static MARKET_JSON_PATH = config.BISQ.DATA_PATH;
private static MARKET_JSON_FILE_PATHS = {
activeCryptoCurrency: '/active_crypto_currency_list.json',
activeFiatCurrency: '/active_fiat_currency_list.json',
cryptoCurrency: '/crypto_currency_list.json',
fiatCurrency: '/fiat_currency_list.json',
offers: '/offers_statistics.json',
trades: '/trade_statistics.json',
};
private cryptoCurrencyLastMtime = new Date('2016-01-01');
private fiatCurrencyLastMtime = new Date('2016-01-01');
private offersLastMtime = new Date('2016-01-01');
private tradesLastMtime = new Date('2016-01-01');
private subdirectoryWatcher: fs.FSWatcher | undefined;
constructor() {}
startBisqService(): void {
try {
this.checkForBisqDataFolder();
} catch (e) {
logger.info('Retrying to start bisq service (markets) in 3 minutes');
setTimeout(this.startBisqService.bind(this), 180000);
return;
}
this.loadBisqDumpFile();
this.startBisqDirectoryWatcher();
}
private checkForBisqDataFolder() {
if (!fs.existsSync(Bisq.MARKET_JSON_PATH + Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency)) {
logger.err(Bisq.MARKET_JSON_PATH + Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency + ` doesn't exist. Make sure Bisq is running and the config is correct before starting the server.`);
throw new Error(`Cannot load BISQ ${Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency} file`);
}
}
private startBisqDirectoryWatcher() {
if (this.subdirectoryWatcher) {
this.subdirectoryWatcher.close();
}
if (!fs.existsSync(Bisq.MARKET_JSON_PATH + Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency)) {
logger.warn(Bisq.MARKET_JSON_PATH + Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency + ` doesn't exist. Trying to restart sub directory watcher again in 3 minutes.`);
setTimeout(() => this.startBisqDirectoryWatcher(), 180000);
return;
}
let fsWait: NodeJS.Timeout | null = null;
this.subdirectoryWatcher = fs.watch(Bisq.MARKET_JSON_PATH, () => {
if (fsWait) {
clearTimeout(fsWait);
}
fsWait = setTimeout(() => {
logger.debug(`Change detected in the Bisq market data folder.`);
this.loadBisqDumpFile();
}, Bisq.FOLDER_WATCH_CHANGE_DETECTION_DEBOUNCE);
});
}
private async loadBisqDumpFile(): Promise<void> {
const start = new Date().getTime();
try {
let marketsDataUpdated = false;
const cryptoMtime = this.getFileMtime(Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency);
const fiatMtime = this.getFileMtime(Bisq.MARKET_JSON_FILE_PATHS.fiatCurrency);
if (cryptoMtime > this.cryptoCurrencyLastMtime || fiatMtime > this.fiatCurrencyLastMtime) {
const cryptoCurrencyData = await this.loadData<Currency[]>(Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency);
const fiatCurrencyData = await this.loadData<Currency[]>(Bisq.MARKET_JSON_FILE_PATHS.fiatCurrency);
const activeCryptoCurrencyData = await this.loadData<Currency[]>(Bisq.MARKET_JSON_FILE_PATHS.activeCryptoCurrency);
const activeFiatCurrencyData = await this.loadData<Currency[]>(Bisq.MARKET_JSON_FILE_PATHS.activeFiatCurrency);
logger.debug('Updating Bisq Market Currency Data');
bisqMarket.setCurrencyData(cryptoCurrencyData, fiatCurrencyData, activeCryptoCurrencyData, activeFiatCurrencyData);
if (cryptoMtime > this.cryptoCurrencyLastMtime) {
this.cryptoCurrencyLastMtime = cryptoMtime;
}
if (fiatMtime > this.fiatCurrencyLastMtime) {
this.fiatCurrencyLastMtime = fiatMtime;
}
marketsDataUpdated = true;
}
const offersMtime = this.getFileMtime(Bisq.MARKET_JSON_FILE_PATHS.offers);
if (offersMtime > this.offersLastMtime) {
const offersData = await this.loadData<OffersData[]>(Bisq.MARKET_JSON_FILE_PATHS.offers);
logger.debug('Updating Bisq Market Offers Data');
bisqMarket.setOffersData(offersData);
this.offersLastMtime = offersMtime;
marketsDataUpdated = true;
}
const tradesMtime = this.getFileMtime(Bisq.MARKET_JSON_FILE_PATHS.trades);
if (tradesMtime > this.tradesLastMtime) {
const tradesData = await this.loadData<TradesData[]>(Bisq.MARKET_JSON_FILE_PATHS.trades);
logger.debug('Updating Bisq Market Trades Data');
bisqMarket.setTradesData(tradesData);
this.tradesLastMtime = tradesMtime;
marketsDataUpdated = true;
}
if (marketsDataUpdated) {
bisqMarket.updateCache();
const time = new Date().getTime() - start;
logger.debug('Bisq market data updated in ' + time + ' ms');
}
} catch (e) {
logger.err('loadBisqMarketDataDumpFile() error.' + (e instanceof Error ? e.message : e));
}
}
private getFileMtime(path: string): Date {
const stats = fs.statSync(Bisq.MARKET_JSON_PATH + path);
return stats.mtime;
}
private loadData<T>(path: string): Promise<T> {
return new Promise((resolve, reject) => {
fs.readFile(Bisq.MARKET_JSON_PATH + path, 'utf8', (err, data) => {
if (err) {
reject(err);
}
try {
const parsedData = JSON.parse(data);
resolve(parsedData);
} catch (e) {
reject('JSON parse error (' + path + ')');
}
});
});
}
}
export default new Bisq();

File diff suppressed because it is too large Load Diff

View File

@@ -37,60 +37,6 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'replacements', this.getRbfReplacements)
.get(config.MEMPOOL.API_URL_PREFIX + 'fullrbf/replacements', this.getFullRbfReplacements)
.post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', this.$postTransactionForm)
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations`, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'donations/images/:id', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations/images/${req.params.id}`, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors`, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors/images/:id', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors/images/${req.params.id}`, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'translators', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators`, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'translators/images/:id', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators/images/${req.params.id}`, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks', this.getBlocks.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
@@ -418,7 +364,7 @@ class BitcoinRoutes {
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await blocks.$getBlocks(height, 15));
} else { // Liquid, Bisq
} else { // Liquid
return await this.getLegacyBlocks(req, res);
}
} catch (e) {
@@ -428,7 +374,7 @@ class BitcoinRoutes {
private async getBlocksByBulk(req: Request, res: Response) {
try {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid, Bisq - Not implemented
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid - Not implemented
return res.status(404).send(`This API is only available for Bitcoin networks`);
}
if (config.MEMPOOL.MAX_BLOCKS_BULK_QUERY <= 0) {

View File

@@ -29,6 +29,7 @@ import websocketHandler from './websocket-handler';
import redisCache from './redis-cache';
import rbfCache from './rbf-cache';
import { calcBitsDifference } from './difficulty-adjustment';
import AccelerationRepository from '../repositories/AccelerationRepository';
class Blocks {
private blocks: BlockExtended[] = [];
@@ -872,6 +873,7 @@ class Blocks {
await BlocksRepository.$deleteBlocksFrom(lastBlock.height - 10);
await HashratesRepository.$deleteLastEntries();
await cpfpRepository.$deleteClustersFrom(lastBlock.height - 10);
await AccelerationRepository.$deleteAccelerationsFrom(lastBlock.height - 10);
this.blocks = this.blocks.slice(0, -10);
this.updateTimerProgress(timer, `rolled back chain divergence from ${this.currentBlockHeight}`);
for (let i = 10; i >= 0; --i) {
@@ -974,6 +976,9 @@ class Blocks {
if (this.blocks.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) {
this.blocks = this.blocks.slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4);
}
blockSummary.transactions.forEach(tx => {
delete tx.acc;
});
this.blockSummaries.push(blockSummary);
if (this.blockSummaries.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) {
this.blockSummaries = this.blockSummaries.slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4);
@@ -1117,6 +1122,7 @@ class Blocks {
}
return {
txid: tx.txid,
time: tx.firstSeen,
fee: tx.fee || 0,
vsize: tx.vsize,
value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0)),

View File

@@ -373,6 +373,21 @@ export class Common {
].includes(pubkey);
}
static isInscription(vin, flags): bigint {
// in taproot, if the last witness item begins with 0x50, it's an annex
const hasAnnex = vin.witness?.[vin.witness.length - 1].startsWith('50');
// script spends have more than one witness item, not counting the annex (if present)
if (vin.witness.length > (hasAnnex ? 2 : 1)) {
// the script itself is the second-to-last witness item, not counting the annex
const asm = vin.inner_witnessscript_asm || transactionUtils.convertScriptSigAsm(vin.witness[vin.witness.length - (hasAnnex ? 3 : 2)]);
// inscriptions smuggle data within an 'OP_0 OP_IF ... OP_ENDIF' envelope
if (asm?.includes('OP_0 OP_IF')) {
flags |= TransactionFlags.inscription;
}
}
return flags;
}
static getTransactionFlags(tx: TransactionExtended): number {
let flags = tx.flags ? BigInt(tx.flags) : 0n;
@@ -409,30 +424,31 @@ export class Common {
if (vin.sequence < 0xfffffffe) {
rbf = true;
}
switch (vin.prevout?.scriptpubkey_type) {
case 'p2pk': flags |= TransactionFlags.p2pk; break;
case 'multisig': flags |= TransactionFlags.p2ms; break;
case 'p2pkh': flags |= TransactionFlags.p2pkh; break;
case 'p2sh': flags |= TransactionFlags.p2sh; break;
case 'v0_p2wpkh': flags |= TransactionFlags.p2wpkh; break;
case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break;
case 'v1_p2tr': {
if (!vin.witness?.length) {
throw new Error('Taproot input missing witness data');
}
flags |= TransactionFlags.p2tr;
// in taproot, if the last witness item begins with 0x50, it's an annex
const hasAnnex = vin.witness?.[vin.witness.length - 1].startsWith('50');
// script spends have more than one witness item, not counting the annex (if present)
if (vin.witness.length > (hasAnnex ? 2 : 1)) {
// the script itself is the second-to-last witness item, not counting the annex
const asm = vin.inner_witnessscript_asm || transactionUtils.convertScriptSigAsm(vin.witness[vin.witness.length - (hasAnnex ? 3 : 2)]);
// inscriptions smuggle data within an 'OP_0 OP_IF ... OP_ENDIF' envelope
if (asm?.includes('OP_0 OP_IF')) {
flags |= TransactionFlags.inscription;
if (vin.prevout?.scriptpubkey_type) {
switch (vin.prevout?.scriptpubkey_type) {
case 'p2pk': flags |= TransactionFlags.p2pk; break;
case 'multisig': flags |= TransactionFlags.p2ms; break;
case 'p2pkh': flags |= TransactionFlags.p2pkh; break;
case 'p2sh': flags |= TransactionFlags.p2sh; break;
case 'v0_p2wpkh': flags |= TransactionFlags.p2wpkh; break;
case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break;
case 'v1_p2tr': {
if (!vin.witness?.length) {
throw new Error('Taproot input missing witness data');
}
flags |= TransactionFlags.p2tr;
flags = Common.isInscription(vin, flags);
} break;
}
} else {
// no prevouts, optimistically check witness-bearing inputs
if (vin.witness?.length >= 2) {
try {
flags = Common.isInscription(vin, flags);
} catch {
// witness script parsing will fail if this isn't really a taproot output
}
} break;
}
}
// sighash flags

View File

@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2';
class DatabaseMigration {
private static currentVersion = 76;
private static currentVersion = 82;
private queryTimeout = 3600_000;
private statisticsAddedIndexed = false;
private uniqueLogs: string[] = [];
@@ -659,6 +659,44 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD prioritized_txs JSON DEFAULT "[]"');
await this.updateToSchemaVersion(76);
}
if (databaseSchemaVersion < 77 && config.MEMPOOL.NETWORK === 'mainnet') {
await this.$executeQuery('ALTER TABLE `accelerations` ADD requested datetime DEFAULT NULL');
await this.updateToSchemaVersion(77);
}
if (databaseSchemaVersion < 78) {
await this.$executeQuery('ALTER TABLE `prices` CHANGE `time` `time` datetime NOT NULL');
await this.updateToSchemaVersion(78);
}
if (databaseSchemaVersion < 79 && isBitcoin === true) {
await this.$executeQuery('TRUNCATE hashrates');
await this.updateToSchemaVersion(79);
}
if (databaseSchemaVersion < 80 && isBitcoin === true) {
await this.$executeQuery('TRUNCATE difficulty_adjustments');
await this.updateToSchemaVersion(80);
}
if (databaseSchemaVersion < 81 && isBitcoin === true) {
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
await this.updateToSchemaVersion(81);
}
if (databaseSchemaVersion < 82 && config.MEMPOOL.NETWORK === 'liquid') {
await this.$executeQuery('TRUNCATE TABLE elements_pegs');
await this.$executeQuery('TRUNCATE TABLE federation_txos');
await this.$executeQuery('SET FOREIGN_KEY_CHECKS = 0');
await this.$executeQuery('TRUNCATE TABLE federation_addresses');
await this.$executeQuery('SET FOREIGN_KEY_CHECKS = 1');
await this.$executeQuery(`INSERT INTO federation_addresses (bitcoinaddress) VALUES ('bc1qxvay4an52gcghxq5lavact7r6qe9l4laedsazz8fj2ee2cy47tlqff4aj4')`); // Federation change address
await this.$executeQuery(`INSERT INTO federation_addresses (bitcoinaddress) VALUES ('3EiAcrzq1cELXScc98KeCswGWZaPGceT1d')`); // Federation change address
await this.$executeQuery(`UPDATE state SET number = 0 WHERE name = 'last_elements_block';`);
await this.$executeQuery(`UPDATE state SET number = 0 WHERE name = 'last_bitcoin_block_audit';`);
await this.updateToSchemaVersion(71);
}
}
/**

View File

@@ -48,6 +48,14 @@ class NodesRoutes {
'032850492ee61a5f7006a2fda6925e4b4ec3782f2b6de2ff0e439ef5a38c3b2470',
'022c80bace98831c44c32fb69755f2b353434e0ee9e7fbda29507f7ef8abea1421',
'02c3559c833e6f99f9ca05fe503e0b4e7524dea9121344edfd3e811101e0c28680',
'02b36a324fa2dd3af2a63ac65f241907882829bed5002b4e14171d25c219e0d470',
'0231b6e8f21f9f6c057f6bf8a812f79e396ee16a66ece91939a1576ce9fb9e87a5',
'034b6aac206bffcbd651b7ead1ab8a0991c945dfafe19ff27dcdeadc6843ebd15c',
'039c065f7e344acd969ebdd4a94550915b6f24e8782ae2be540bb96c8a4fcfb86b',
'03d9f9f4803fc75920f14dd13d83fbecc53229a65d4ee4cd2d86fdf211f7337576',
'0357fe48c4dece744f70865eda66e396aab5d05e09e1145cd3b7da83f11446d4cf',
'02bca4d642eda631f2c8659758e2a2868e518b93503f2bfcd767749c6530a10679',
'03f32c99c0bb9f62dae53671d1d300565773455248f34134cc02779b881561174e',
'032c7c7819276c4f706a04df1a0f1e10a5495994a7be4c1d3d28ca766e5a2b957b',
'025a7e38c2834dd843591a4d23d5f09cdeb77ddca85f673c2d944a14220ff14cf7',
'0395e2731a1673ef21d7a16a727c4fc4d4c35a861c428ce2c819c53d2b81c8bd55',
@@ -60,6 +68,14 @@ class NodesRoutes {
'039c14fdec2d958e3d14cebf657451bbd9e039196615785e82c917f274e3fb2205',
'033589bbcb233ffc416cefd5437c7f37e9d7cb7942d405e39e72c4c846d9b37f18',
'029293110441c6e2eacb57e1255bf6ef05c41a6a676fe474922d33c19f98a7d584',
'038eb09bed4532ff36d12acc1279f55cbe8d95212d19f809e057bb50de00051fba',
'027b7c0278366a0268e8bd0072b14539f6cb455a7bd588ae22d888bed541f65311',
'02f4dd78f6eda8838029b2cdbaaea6e875e2fa373cd348ee41a7c1bb177d3fca66',
'036b3fb692da214a3edaac5b67903b958f5ccd8712e09aa61b67ea7acfd94b40c2',
'023bc8915d308e0b65f8de6867f95960141372436fce3edad5cec3f364d6ac948f',
'0341690503ef21d0e203dddd9e62646380d0dfc32c499e055e7f698b9064d1c736',
'0355d573805c018a37a5b2288378d70e9b5b438f7394abd6f467cb9b47c90eeb93',
'0361aa68deb561a8b47b41165848edcccb98a1b56a5ea922d9d5b30a09bb7282ea',
'0235ad0b56ed8c42c4354444c24e971c05e769ec0b5fb0ccea42880095dc02ea2c',
'029700819a37afea630f80e6cc461f3fd3c4ace2598a21cfbbe64d1c78d0ee69a5',
'02c2d8b2dbf87c7894af2f1d321290e2fe6db5446cd35323987cee98f06e2e0075',
@@ -76,6 +92,14 @@ class NodesRoutes {
'0243348cb3741cfe2d8485fa8375c29c7bc7cbb67577c363cb6987a5e5fd0052cc',
'02cb73e631af44bee600d80f8488a9194c9dc5c7590e575c421a070d1be05bc8e9',
'0306f55ee631aa1e2cd4d9b2bfcbc14404faec5c541cef8b2e6f779061029d09c4',
'030bbbd8495561a894e301fe6ba5b22f8941fc661cc0e673e0206158231d8ac130',
'03ee1f08e516ed083475f39c6cae4fa1eec686d004d2f105218269e27d7f2da5a4',
'028c378b998f476ed22d6815c170dd2a3388a43fdf791a7cff70b9997349b8447a',
'036f19f044d19cb1b04f14d91b6e7e5443ce337217a8c14d43861f3e86dd07bd7f',
'03058d61869e8b88436493648b2e3e530627edf5a0b253c285cd565c1477a5c237',
'0279dfedc87b47a941f1797f2c422c03aa3108914ea6b519d76537d60860535a9a',
'0353486b8016761e58ec8aee7305ee58d5dc66b55ef5bd8cbaf49508f66d52d62e',
'03df5db8eccfabcae47ff15553cfdecb2d3f56979f43a0c3578f28d056b5e35104',
'03ddab321b760433cbf561b615ef62ac7d318630c5f51d523aaf5395b90b751956',
'033d92c7bfd213ef1b34c90e985fb5dc77f9ec2409d391492484e57a44c4aca1de',
'02ad010dda54253c1eb9efe38b0760657a3b43ecad62198c359c051c9d99d45781',
@@ -88,6 +112,14 @@ class NodesRoutes {
'038310e3a786340f2bd7770704c7ccfe560fd163d9a1c99d67894597419d12cbf7',
'03e5e9d879b72c7d67ecd483bae023bd33e695bb32b981a4021260f7b9d62bc761',
'028d16e1a0ace4c0c0a421536d8d32ce484dfe6e2f726b7b0e7c30f12a195f8cc7',
'0326cf9a4ca67a5b9cdffae57293dbd6f7c5113b93010dc6f6fe4af3afde1a1739',
'034867e16f62cebb8c2c2c22b91117c173bbece9c8a1e5bd001374a3699551cd8f',
'038dfb1f1b637a8c27e342ffc6f9feca20e0b47be3244e09ae78df4998e2ae83b9',
'03cb1cea3394d973355c11bc61c2f689f9d3e1c3db60d205f27770f5ad83200f77',
'03535447b592cbdb153189b3e06a455452b1011380cb3e6511a31090c15d8efc9f',
'028e90e9984d262ebfa3c23fb3f335a2ae061a0bdedee03f45f72b438d9e7d2ce3',
'03ee0176289dc4a6111fa5ef22eed5273758c420fbe58cc1d2d76def75dd7e640c',
'0370b2cd9f0eaf436d5c25c93fb39210d8cc06b31f688fc2f54418aabe394aed79',
'02ff690d06c187ab994bf83c5a2114fe5bf50112c2c817af0f788f736be9fa2070',
'02a9f570c51a2526a5ee85802e88f9281bed771eb66a0c8a7d898430dd5d0eae45',
'038c3de773255d3bd7a50e31e58d423baac5c90826a74d75e64b74c95475de1097',
@@ -104,6 +136,14 @@ class NodesRoutes {
'03229ab4b7f692753e094b93df90530150680f86b535b5183b0cffd75b3df583fc',
'03a696eb7acde991c1be97a58a9daef416659539ae462b897f5e9ae361f990228e',
'0248bf26cf3a63ab8870f34dc0ec9e6c8c6288cdba96ba3f026f34ec0f13ac4055',
'021b28ecdd782fd909705d6be354db268977b1a2ac5a5275186fc19e08bb8fca93',
'031bec1fbd8eb7fe94d2bda108c9c3cc8c22ecfc1c3a5c11d36f5881b01b4a81a6',
'03879c4f827a3188574d5757e002f574265a966d70aea942169785b31369b067d5',
'0228d4b5a4fd73a03967b76f8b8cb37b9d0b6e7039126a9397bb732c15bed78e9b',
'03f58dbb629f4427f5a1dbc02e6a7ec79345fdf13a0e4163d4f3b7aea2539cf095',
'021cdcb8123aa670cdfc9f43909dbb297363c093883409e9e7fc82e7267f7c72bd',
'02f2aa2c2b7b432a70dc4d0b04afa19d48715ed3b90594d49c1c8744f2e9ebb030',
'03709a02fb3ab4857689a8ea0bd489a6ab6f56f8a397be578bc6d5ad22efbe3756',
'03fbc17549ec667bccf397ababbcb4cdc0e3394345e4773079ab2774612ec9be61',
'03da9a8623241ccf95f19cd645c6cecd4019ac91570e976eb0a128bebbc4d8a437',
'03ca5340cf85cb2e7cf076e489f785410838de174e40be62723e8a60972ad75144',
@@ -116,6 +156,14 @@ class NodesRoutes {
'02b6b1640fe029e304c216951af9fbefdb23b0bdc9baaf327540d31b6107841fdf',
'03694289827203a5b3156d753071ddd5bf92e371f5a462943f9555eef6d2d6606c',
'0283d850db7c3e8ea7cc9c4abc7afaab12bbdf72b677dcba1d608350d2537d7d43',
'03b4dda7878d3b7b71ecd6d4738322c7f9a9c1fb583374d2724f4ccc4947f37570',
'0279a35f05b5acf159429549e56fd426685c4fec191431c58738968bbc77a39f25',
'03cb102d796ddcf08610cd03fae8b7a1df69ff48e9e8a152af315f9edf71762eb8',
'036b89526f4d5ac4c317f4fd23cb9f8e4ad844498bc7950a41114d060101d995d4',
'0313eade145959d7036db009fd5b0bf1947a739c7c3c790b491ec9161b94e6ad1e',
'02b670ca4c4bb2c5ea89c3b691da98a194cfc48fcd5c072df02a20290bddd60610',
'02a9196d5e08598211397a83cf013a5962b84bd61198abfdd204dff987e54f7a0d',
'036d015cd2f486fb38348182980b7e596e6c9733873102ea126fed7b4152be03b8',
'02521287789f851268a39c9eccc9d6180d2c614315b583c9e6ae0addbd6d79df06',
'0258c2a7b7f8af2585b4411b1ec945f70988f30412bb1df179de941f14d0b1bc3e',
'03c3389ff1a896f84d921ed01a19fc99c6724ce8dc4b960cd3b7b2362b62cd60d7',

View File

@@ -306,7 +306,7 @@ class ElementsParser {
for (const utxo of unspentAsTip) {
if (utxo.expiredAt === 0 && block.height >= utxo.blocknumber + utxo.timelock) { // The UTXO is expiring in this block
await DB.query(`UPDATE federation_txos SET unspent = 0, lastblockupdate = ?, expiredAt = ? WHERE txid = ? AND txindex = ?`, [confirmedTip, block.time, utxo.txid, utxo.txindex]);
await DB.query(`UPDATE federation_txos SET lastblockupdate = ?, expiredAt = ? WHERE txid = ? AND txindex = ?`, [confirmedTip, block.time, utxo.txid, utxo.txindex]);
} else if (utxo.expiredAt === 0 && confirmedTip >= utxo.blocknumber + utxo.timelock) { // The UTXO is expiring before the tip: we need to keep track of it
await DB.query(`UPDATE federation_txos SET lastblockupdate = ? WHERE txid = ? AND txindex = ?`, [utxo.blocknumber + utxo.timelock - 1, utxo.txid, utxo.txindex]);
} else {

View File

@@ -343,7 +343,7 @@ class MempoolBlocks {
if (txid in mempool) {
mempool[txid].cpfpDirty = (rate !== mempool[txid].effectiveFeePerVsize);
mempool[txid].effectiveFeePerVsize = rate;
mempool[txid].cpfpChecked = false;
mempool[txid].cpfpChecked = true;
}
}

View File

@@ -5,6 +5,9 @@ import axios from 'axios';
export interface Acceleration {
txid: string,
added: number,
effectiveVsize: number,
effectiveFee: number,
feeDelta: number,
pools: number[],
};

View File

@@ -44,7 +44,7 @@ const wantable = [
];
class WebsocketHandler {
private wss: WebSocket.Server | undefined;
private webSocketServers: WebSocket.Server[] = [];
private extraInitProperties = {};
private numClients = 0;
@@ -54,11 +54,12 @@ class WebsocketHandler {
private socketData: { [key: string]: string } = {};
private serializedInitData: string = '{}';
private lastRbfSummary: ReplacementInfo[] | null = null;
private mempoolSequence: number = 0;
constructor() { }
setWebsocketServer(wss: WebSocket.Server) {
this.wss = wss;
addWebsocketServer(wss: WebSocket.Server) {
this.webSocketServers.push(wss);
}
setExtraInitData(property: string, value: any) {
@@ -102,11 +103,13 @@ class WebsocketHandler {
}
setupConnectionHandling() {
if (!this.wss) {
throw new Error('WebSocket.Server is not set');
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set');
}
this.wss.on('connection', (client: WebSocket, req) => {
// TODO - Fix indentation after PR is merged
for (const server of this.webSocketServers) {
server.on('connection', (client: WebSocket, req) => {
this.numConnected++;
client['remoteAddress'] = req.headers['x-forwarded-for'] || req.socket?.remoteAddress || 'unknown';
client.on('error', (e) => {
@@ -315,6 +318,7 @@ class WebsocketHandler {
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
response['projected-block-transactions'] = JSON.stringify({
index: index,
sequence: this.mempoolSequence,
blockTransactions: (mBlocksWithTransactions[index]?.transactions || []).map(mempoolBlocks.compressTx),
});
} else {
@@ -360,14 +364,6 @@ class WebsocketHandler {
client['track-donation'] = parsedMessage['track-donation'];
}
if (parsedMessage['track-bisq-market']) {
if (/^[a-z]{3}_[a-z]{3}$/.test(parsedMessage['track-bisq-market'])) {
client['track-bisq-market'] = parsedMessage['track-bisq-market'];
} else {
client['track-bisq-market'] = null;
}
}
if (Object.keys(response).length) {
client.send(this.serializeResponse(response));
}
@@ -377,14 +373,17 @@ class WebsocketHandler {
}
});
});
}
}
handleNewDonation(id: string) {
if (!this.wss) {
throw new Error('WebSocket.Server is not set');
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set');
}
this.wss.clients.forEach((client) => {
// TODO - Fix indentation after PR is merged
for (const server of this.webSocketServers) {
server.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
@@ -392,43 +391,50 @@ class WebsocketHandler {
client.send(JSON.stringify({ donationConfirmed: true }));
}
});
}
}
handleLoadingChanged(indicators: ILoadingIndicators) {
if (!this.wss) {
throw new Error('WebSocket.Server is not set');
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set');
}
this.updateSocketDataFields({ 'loadingIndicators': indicators });
const response = JSON.stringify({ loadingIndicators: indicators });
this.wss.clients.forEach((client) => {
// TODO - Fix indentation after PR is merged
for (const server of this.webSocketServers) {
server.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
client.send(response);
});
}
}
handleNewConversionRates(conversionRates: ApiPrice) {
if (!this.wss) {
throw new Error('WebSocket.Server is not set');
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set');
}
this.updateSocketDataFields({ 'conversions': conversionRates });
const response = JSON.stringify({ conversions: conversionRates });
this.wss.clients.forEach((client) => {
// TODO - Fix indentation after PR is merged
for (const server of this.webSocketServers) {
server.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
client.send(response);
});
}
}
handleNewStatistic(stats: OptimizedStatistic) {
if (!this.wss) {
throw new Error('WebSocket.Server is not set');
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set');
}
this.printLogs();
@@ -437,7 +443,9 @@ class WebsocketHandler {
'live-2h-chart': stats
});
this.wss.clients.forEach((client) => {
// TODO - Fix indentation after PR is merged
for (const server of this.webSocketServers) {
server.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
@@ -448,11 +456,12 @@ class WebsocketHandler {
client.send(response);
});
}
}
handleReorg(): void {
if (!this.wss) {
throw new Error('WebSocket.Server is not set');
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set');
}
const da = difficultyAdjustment.getDifficultyAdjustment();
@@ -463,7 +472,9 @@ class WebsocketHandler {
'da': da?.previousTime ? da : undefined,
});
this.wss.clients.forEach((client) => {
// TODO - Fix indentation after PR is merged
for (const server of this.webSocketServers) {
server.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
@@ -481,13 +492,14 @@ class WebsocketHandler {
client.send(this.serializeResponse(response));
}
});
}
}
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[],
candidates?: GbtCandidates): Promise<void> {
if (!this.wss) {
throw new Error('WebSocket.Server is not set');
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set');
}
this.printLogs();
@@ -560,7 +572,9 @@ class WebsocketHandler {
// pre-compute new tracked outspends
const outspendCache: { [txid: string]: { [vout: number]: { vin: number, txid: string } } } = {};
const trackedTxs = new Set<string>();
this.wss.clients.forEach((client) => {
// TODO - Fix indentation after PR is merged
for (const server of this.webSocketServers) {
server.clients.forEach((client) => {
if (client['track-tx']) {
trackedTxs.add(client['track-tx']);
}
@@ -570,6 +584,7 @@ class WebsocketHandler {
}
}
});
}
if (trackedTxs.size > 0) {
for (const tx of newTransactions) {
for (let i = 0; i < tx.vin.length; i++) {
@@ -589,7 +604,13 @@ class WebsocketHandler {
const addressCache = this.makeAddressCache(newTransactions);
const removedAddressCache = this.makeAddressCache(deletedTransactions);
this.wss.clients.forEach(async (client) => {
if (memPool.isInSync()) {
this.mempoolSequence++;
}
// TODO - Fix indentation after PR is merged
for (const server of this.webSocketServers) {
server.clients.forEach(async (client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
@@ -747,7 +768,7 @@ class WebsocketHandler {
accelerated: mempoolTx.acceleration || undefined,
}
};
if (!mempoolTx.cpfpChecked) {
if (!mempoolTx.cpfpChecked && !mempoolTx.acceleration) {
calculateCpfp(mempoolTx, newMempool);
}
if (mempoolTx.cpfpDirty) {
@@ -810,6 +831,7 @@ class WebsocketHandler {
if (mBlockDeltas[index]) {
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-${index}`, {
index: index,
sequence: this.mempoolSequence,
delta: mBlockDeltas[index],
});
}
@@ -829,11 +851,12 @@ class WebsocketHandler {
client.send(this.serializeResponse(response));
}
});
}
}
async handleNewBlock(block: BlockExtended, txIds: string[], transactions: MempoolTransactionExtended[]): Promise<void> {
if (!this.wss) {
throw new Error('WebSocket.Server is not set');
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set');
}
this.printLogs();
@@ -977,7 +1000,13 @@ class WebsocketHandler {
return responseCache[key];
}
this.wss.clients.forEach((client) => {
if (memPool.isInSync()) {
this.mempoolSequence++;
}
// TODO - Fix indentation after PR is merged
for (const server of this.webSocketServers) {
server.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
@@ -1143,11 +1172,13 @@ class WebsocketHandler {
if (mBlockDeltas[index].added.length > (mBlocksWithTransactions[index]?.transactions.length / 2)) {
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-full-${index}`, {
index: index,
sequence: this.mempoolSequence,
blockTransactions: mBlocksWithTransactions[index].transactions.map(mempoolBlocks.compressTx),
});
} else {
response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-delta-${index}`, {
index: index,
sequence: this.mempoolSequence,
delta: mBlockDeltas[index],
});
}
@@ -1158,6 +1189,7 @@ class WebsocketHandler {
client.send(this.serializeResponse(response));
}
});
}
await statistics.runStatistics();
}
@@ -1239,13 +1271,15 @@ class WebsocketHandler {
}
private printLogs(): void {
if (this.wss) {
if (this.webSocketServers.length) {
let numTxSubs = 0;
let numTxsSubs = 0;
let numProjectedSubs = 0;
let numRbfSubs = 0;
this.wss.clients.forEach((client) => {
// TODO - Fix indentation after PR is merged
for (const server of this.webSocketServers) {
server.clients.forEach((client) => {
if (client['track-tx']) {
numTxSubs++;
}
@@ -1259,8 +1293,12 @@ class WebsocketHandler {
numRbfSubs++;
}
})
}
const count = this.wss?.clients?.size || 0;
let count = 0;
for (const server of this.webSocketServers) {
count += server.clients?.size || 0;
}
const diff = count - this.numClients;
this.numClients = count;
logger.debug(`${count} websocket clients | ${this.numConnected} connected | ${this.numDisconnected} disconnected | (${diff >= 0 ? '+' : ''}${diff})`);

View File

@@ -9,6 +9,7 @@ interface IConfig {
NETWORK: 'mainnet' | 'testnet' | 'signet' | 'liquid' | 'liquidtestnet';
BACKEND: 'esplora' | 'electrum' | 'none';
HTTP_PORT: number;
UNIX_SOCKET_PATH: string;
SPAWN_CLUSTER_PROCS: number;
API_URL_PREFIX: string;
POLL_RATE_MS: number;
@@ -116,10 +117,6 @@ interface IConfig {
ENABLED: boolean;
TX_PER_SECOND_SAMPLE_PERIOD: number;
};
BISQ: {
ENABLED: boolean;
DATA_PATH: string;
};
SOCKS5PROXY: {
ENABLED: boolean;
USE_ONION: boolean;
@@ -133,8 +130,6 @@ interface IConfig {
MEMPOOL_ONION: string;
LIQUID_API: string;
LIQUID_ONION: string;
BISQ_URL: string;
BISQ_ONION: string;
};
MAXMIND: {
ENABLED: boolean;
@@ -159,6 +154,7 @@ interface IConfig {
},
FIAT_PRICE: {
ENABLED: boolean;
PAID: boolean;
API_KEY: string;
},
}
@@ -170,6 +166,7 @@ const defaults: IConfig = {
'NETWORK': 'mainnet',
'BACKEND': 'none',
'HTTP_PORT': 8999,
'UNIX_SOCKET_PATH': '',
'SPAWN_CLUSTER_PROCS': 0,
'API_URL_PREFIX': '/api/v1/',
'POLL_RATE_MS': 2000,
@@ -258,10 +255,6 @@ const defaults: IConfig = {
'ENABLED': true,
'TX_PER_SECOND_SAMPLE_PERIOD': 150
},
'BISQ': {
'ENABLED': false,
'DATA_PATH': '/bisq/statsnode-data/btc_mainnet/db'
},
'LIGHTNING': {
'ENABLED': false,
'BACKEND': 'lnd',
@@ -293,9 +286,7 @@ const defaults: IConfig = {
'MEMPOOL_API': 'https://mempool.space/api/v1',
'MEMPOOL_ONION': 'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1',
'LIQUID_API': 'https://liquid.network/api/v1',
'LIQUID_ONION': 'http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1',
'BISQ_URL': 'https://bisq.markets/api',
'BISQ_ONION': 'http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api'
'LIQUID_ONION': 'http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1'
},
'MAXMIND': {
'ENABLED': false,
@@ -320,6 +311,7 @@ const defaults: IConfig = {
},
'FIAT_PRICE': {
'ENABLED': true,
'PAID': false,
'API_KEY': '',
},
};
@@ -333,7 +325,6 @@ class Config implements IConfig {
DATABASE: IConfig['DATABASE'];
SYSLOG: IConfig['SYSLOG'];
STATISTICS: IConfig['STATISTICS'];
BISQ: IConfig['BISQ'];
LIGHTNING: IConfig['LIGHTNING'];
LND: IConfig['LND'];
CLIGHTNING: IConfig['CLIGHTNING'];
@@ -355,7 +346,6 @@ class Config implements IConfig {
this.DATABASE = configs.DATABASE;
this.SYSLOG = configs.SYSLOG;
this.STATISTICS = configs.STATISTICS;
this.BISQ = configs.BISQ;
this.LIGHTNING = configs.LIGHTNING;
this.LND = configs.LND;
this.CLIGHTNING = configs.CLIGHTNING;

View File

@@ -11,8 +11,6 @@ import memPool from './api/mempool';
import diskCache from './api/disk-cache';
import statistics from './api/statistics/statistics';
import websocketHandler from './api/websocket-handler';
import bisq from './api/bisq/bisq';
import bisqMarkets from './api/bisq/markets';
import logger from './logger';
import backendInfo from './api/backend-info';
import loadingIndicators from './api/loading-indicators';
@@ -32,7 +30,6 @@ import networkSyncService from './tasks/lightning/network-sync.service';
import statisticsRoutes from './api/statistics/statistics.routes';
import pricesRoutes from './api/prices/prices.routes';
import miningRoutes from './api/mining/mining-routes';
import bisqRoutes from './api/bisq/bisq.routes';
import liquidRoutes from './api/liquid/liquid.routes';
import bitcoinRoutes from './api/bitcoin/bitcoin.routes';
import fundingTxFetcher from './tasks/lightning/sync-tasks/funding-tx-fetcher';
@@ -46,10 +43,14 @@ import redisCache from './api/redis-cache';
import accelerationApi from './api/services/acceleration';
import bitcoinCoreRoutes from './api/bitcoin/bitcoin-core.routes';
import bitcoinSecondClient from './api/bitcoin/bitcoin-second-client';
import accelerationRoutes from './api/acceleration/acceleration.routes';
import aboutRoutes from './api/about.routes';
class Server {
private wss: WebSocket.Server | undefined;
private wssUnixSocket: WebSocket.Server | undefined;
private server: http.Server | undefined;
private serverUnixSocket: http.Server | undefined;
private app: Application;
private currentBackendRetryInterval = 1;
private backendRetryCount = 0;
@@ -138,6 +139,10 @@ class Server {
this.server = http.createServer(this.app);
this.wss = new WebSocket.Server({ server: this.server });
if (config.MEMPOOL.UNIX_SOCKET_PATH) {
this.serverUnixSocket = http.createServer(this.app);
this.wssUnixSocket = new WebSocket.Server({ server: this.serverUnixSocket });
}
this.setUpWebsocketHandling();
@@ -182,13 +187,6 @@ class Server {
setInterval(() => { this.healthCheck(); }, 2500);
if (config.BISQ.ENABLED) {
bisq.startBisqService();
bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitData('bsq-price', price));
blocks.setNewBlockCallback(bisq.handleNewBitcoinBlock.bind(bisq));
bisqMarkets.startBisqService();
}
if (config.LIGHTNING.ENABLED) {
this.$runLightningBackend();
}
@@ -200,6 +198,16 @@ class Server {
logger.notice(`Mempool Server is running on port ${config.MEMPOOL.HTTP_PORT}`);
}
});
if (this.serverUnixSocket) {
this.serverUnixSocket.listen(config.MEMPOOL.UNIX_SOCKET_PATH, () => {
if (worker) {
logger.info(`Mempool Server worker #${process.pid} started`);
} else {
logger.notice(`Mempool Server is listening on ${config.MEMPOOL.UNIX_SOCKET_PATH}`);
}
});
}
}
async runMainUpdateLoop(): Promise<void> {
@@ -273,8 +281,12 @@ class Server {
setUpWebsocketHandling(): void {
if (this.wss) {
websocketHandler.setWebsocketServer(this.wss);
websocketHandler.addWebsocketServer(this.wss);
}
if (this.wssUnixSocket) {
websocketHandler.addWebsocketServer(this.wssUnixSocket);
}
if (Common.isLiquid() && config.DATABASE.ENABLED) {
blocks.setNewBlockCallback(async () => {
try {
@@ -307,9 +319,6 @@ class Server {
if (Common.indexingEnabled() && config.MEMPOOL.ENABLED) {
miningRoutes.initRoutes(this.app);
}
if (config.BISQ.ENABLED) {
bisqRoutes.initRoutes(this.app);
}
if (Common.isLiquid()) {
liquidRoutes.initRoutes(this.app);
}
@@ -318,6 +327,10 @@ class Server {
nodesRoutes.initRoutes(this.app);
channelsRoutes.initRoutes(this.app);
}
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
accelerationRoutes.initRoutes(this.app);
}
aboutRoutes.initRoutes(this.app);
}
healthCheck(): void {
@@ -345,6 +358,12 @@ class Server {
if (config.DATABASE.ENABLED) {
DB.releasePidLock();
}
this.server?.close();
this.serverUnixSocket?.close();
this.wss?.close();
if (this.wssUnixSocket) {
this.wssUnixSocket.close();
}
process.exit(code);
}

View File

@@ -86,9 +86,6 @@ class Logger {
if (config.LIGHTNING.ENABLED) {
return config.MEMPOOL.NETWORK === 'mainnet' ? 'lightning' : `${config.MEMPOOL.NETWORK}-lightning`;
}
if (config.BISQ.ENABLED) {
return 'bisq';
}
if (config.MEMPOOL.NETWORK && config.MEMPOOL.NETWORK !== 'mainnet') {
return config.MEMPOOL.NETWORK;
}

View File

@@ -435,7 +435,6 @@ export interface WebsocketResponse {
'track-tx': string;
'track-address': string;
'watch-mempool': boolean;
'track-bisq-market': string;
}
export interface VbytesPerSecond {

View File

@@ -1,4 +1,4 @@
import { AccelerationInfo, makeBlockTemplate } from '../api/acceleration';
import { AccelerationInfo, makeBlockTemplate } from '../api/acceleration/acceleration';
import { RowDataPacket } from 'mysql2';
import DB from '../database';
import logger from '../logger';
@@ -6,8 +6,8 @@ import { IEsploraApi } from '../api/bitcoin/esplora-api.interface';
import { Common } from '../api/common';
import config from '../config';
import blocks from '../api/blocks';
import accelerationApi, { Acceleration } from '../api/services/acceleration';
import accelerationCosts from '../api/acceleration';
import accelerationApi, { Acceleration, AccelerationHistory } from '../api/services/acceleration';
import accelerationCosts from '../api/acceleration/acceleration';
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
import transactionUtils from '../api/transaction-utils';
import { BlockExtended, MempoolTransactionExtended } from '../mempool.interfaces';
@@ -15,6 +15,7 @@ import { BlockExtended, MempoolTransactionExtended } from '../mempool.interfaces
export interface PublicAcceleration {
txid: string,
height: number,
added: number,
pool: {
id: number,
slug: string,
@@ -29,15 +30,20 @@ export interface PublicAcceleration {
class AccelerationRepository {
private bidBoostV2Activated = 831580;
public async $saveAcceleration(acceleration: AccelerationInfo, block: IEsploraApi.Block, pool_id: number): Promise<void> {
public async $saveAcceleration(acceleration: AccelerationInfo, block: IEsploraApi.Block, pool_id: number, accelerationData: Acceleration[]): Promise<void> {
const accelerationMap: { [txid: string]: Acceleration } = {};
for (const acc of accelerationData) {
accelerationMap[acc.txid] = acc;
}
try {
await DB.query(`
INSERT INTO accelerations(txid, added, height, pool, effective_vsize, effective_fee, boost_rate, boost_cost)
VALUE (?, FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?)
INSERT INTO accelerations(txid, requested, added, height, pool, effective_vsize, effective_fee, boost_rate, boost_cost)
VALUE (?, FROM_UNIXTIME(?), FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
height = ?
`, [
acceleration.txSummary.txid,
accelerationMap[acceleration.txSummary.txid].added,
block.timestamp,
block.height,
pool_id,
@@ -64,7 +70,7 @@ class AccelerationRepository {
}
let query = `
SELECT * FROM accelerations
SELECT *, UNIX_TIMESTAMP(requested) as requested_timestamp, UNIX_TIMESTAMP(added) as block_timestamp FROM accelerations
JOIN pools on pools.unique_id = accelerations.pool
`;
let params: any[] = [];
@@ -99,6 +105,7 @@ class AccelerationRepository {
return rows.map(row => ({
txid: row.txid,
height: row.height,
added: row.requested_timestamp || row.block_timestamp,
pool: {
id: row.id,
slug: row.slug,
@@ -202,7 +209,7 @@ class AccelerationRepository {
const tx = blockTxs[acc.txid];
const accelerationInfo = accelerationCosts.getAccelerationInfo(tx, boostRate, transactions);
accelerationInfo.cost = Math.max(0, Math.min(acc.feeDelta, accelerationInfo.cost));
this.$saveAcceleration(accelerationInfo, block, block.extras.pool.id);
this.$saveAcceleration(accelerationInfo, block, block.extras.pool.id, successfulAccelerations);
}
}
const lastSyncedHeight = await this.$getLastSyncedHeight();
@@ -230,7 +237,7 @@ class AccelerationRepository {
logger.debug(`Fetching accelerations between block ${lastSyncedHeight} and ${currentHeight}`);
// Fetch accelerations from mempool.space since the last synced block;
const accelerationsByBlock = {};
const accelerationsByBlock: {[height: number]: AccelerationHistory[]} = {};
const blockHashes = {};
let done = false;
let page = 1;
@@ -297,12 +304,16 @@ class AccelerationRepository {
const feeStats = Common.calcEffectiveFeeStatistics(template);
boostRate = feeStats.medianFee;
}
const accelerationSummaries = accelerations.map(acc => ({
...acc,
pools: acc.pools.map(pool => pool.pool_unique_id),
}))
for (const acc of accelerations) {
if (blockTxs[acc.txid]) {
const tx = blockTxs[acc.txid];
const accelerationInfo = accelerationCosts.getAccelerationInfo(tx, boostRate, transactions);
accelerationInfo.cost = Math.max(0, Math.min(acc.feeDelta, accelerationInfo.cost));
await this.$saveAcceleration(accelerationInfo, block, block.extras.pool.id);
await this.$saveAcceleration(accelerationInfo, block, block.extras.pool.id, accelerationSummaries);
}
}
await this.$setLastSyncedHeight(height);
@@ -317,6 +328,26 @@ class AccelerationRepository {
logger.debug(`Indexing accelerations completed`);
}
/**
* Delete accelerations from the database above blockHeight
*/
public async $deleteAccelerationsFrom(blockHeight: number): Promise<void> {
logger.info(`Delete newer accelerations from height ${blockHeight} from the database`);
try {
const currentSyncedHeight = await this.$getLastSyncedHeight();
if (currentSyncedHeight >= blockHeight) {
await DB.query(`
UPDATE state
SET number = ?
WHERE name = 'last_acceleration_block'
`, [blockHeight - 1]);
}
await DB.query(`DELETE FROM accelerations where height >= ${blockHeight}`);
} catch (e) {
logger.err('Cannot delete indexed accelerations. Reason: ' + (e instanceof Error ? e.message : e));
}
}
}
export default new AccelerationRepository();

View File

@@ -1,3 +1,4 @@
import config from '../../config';
import { query } from '../../utils/axios-query';
import { ConversionFeed, ConversionRates } from '../price-updater';
@@ -37,15 +38,26 @@ const emptyRates = {
ZAR: -1,
};
class FreeCurrencyApi implements ConversionFeed {
private API_KEY: string;
constructor(apiKey: string) {
this.API_KEY = apiKey;
type PaidCurrencyData = {
[key: string]: {
code: string;
value: number;
}
};
type FreeCurrencyData = {
[key: string]: number;
};
class FreeCurrencyApi implements ConversionFeed {
private API_KEY = config.FIAT_PRICE.API_KEY;
private PAID = config.FIAT_PRICE.PAID;
private API_URL_PREFIX: string = this.PAID ? `https://api.currencyapi.com/v3/` : `https://api.freecurrencyapi.com/v1/`;
constructor() { }
public async $getQuota(): Promise<any> {
const response = await query(`https://api.freecurrencyapi.com/v1/status?apikey=${this.API_KEY}`);
const response = await query(`${this.API_URL_PREFIX}status?apikey=${this.API_KEY}`);
if (response && response['quotas']) {
return response['quotas'];
}
@@ -53,21 +65,36 @@ class FreeCurrencyApi implements ConversionFeed {
}
public async $fetchLatestConversionRates(): Promise<ConversionRates> {
const response = await query(`https://api.freecurrencyapi.com/v1/latest?apikey=${this.API_KEY}`);
const response = await query(`${this.API_URL_PREFIX}latest?apikey=${this.API_KEY}`);
if (response && response['data']) {
if (this.PAID) {
response['data'] = this.convertData(response['data']);
}
return response['data'];
}
return emptyRates;
}
public async $fetchConversionRates(date: string): Promise<ConversionRates> {
const response = await query(`https://api.freecurrencyapi.com/v1/historical?date=${date}&apikey=${this.API_KEY}`);
if (response && response['data'] && response['data'][date]) {
const response = await query(`${this.API_URL_PREFIX}historical?date=${date}&apikey=${this.API_KEY}`);
if (response && response['data'] && (response['data'][date] || this.PAID)) {
if (this.PAID) {
response['data'] = this.convertData(response['data']);
response['data'][response['meta'].last_updated_at.substr(0, 10)] = response['data'];
}
return response['data'][date];
}
return emptyRates;
}
private convertData(data: PaidCurrencyData): FreeCurrencyData {
const simplifiedData: FreeCurrencyData = {};
for (const key in data) {
simplifiedData[key] = data[key].value;
}
return simplifiedData;
}
}
export default FreeCurrencyApi;

View File

@@ -71,7 +71,7 @@ class PriceUpdater {
this.feeds.push(new BitfinexApi());
this.feeds.push(new GeminiApi());
this.currencyConversionFeed = new FreeCurrencyApi(config.FIAT_PRICE.API_KEY);
this.currencyConversionFeed = new FreeCurrencyApi();
this.setCyclePosition();
}

3
contributors/daweilv.txt Normal file
View File

@@ -0,0 +1,3 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of April 7, 2024.
Signed: daweilv

View File

@@ -323,25 +323,6 @@ Corresponding `docker-compose.yml` overrides:
<br/>
`mempool-config.json`:
```json
"BISQ": {
"ENABLED": false,
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db"
}
```
Corresponding `docker-compose.yml` overrides:
```yaml
api:
environment:
BISQ_ENABLED: ""
BISQ_DATA_PATH: ""
...
```
<br/>
`mempool-config.json`:
```json
"SOCKS5PROXY": {

View File

@@ -11,10 +11,17 @@ RUN apt-get install -y build-essential python3 pkg-config curl ca-certificates
# Install Rust via rustup
RUN CPU_ARCH=$(uname -m); if [ "$CPU_ARCH" = "armv7l" ]; then c_rehash; fi
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable
#RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable
#Workaround to run on github actions from https://github.com/rust-lang/rustup/issues/2700#issuecomment-1367488985
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sed 's#/proc/self/exe#\/bin\/sh#g' | sh -s -- -y --default-toolchain stable
ENV PATH="/root/.cargo/bin:$PATH"
COPY --from=backend . .
COPY --from=rustgbt . ../rust/
ENV FD=/build/rust-gbt
RUN npm install --omit=dev --omit=optional
WORKDIR /build
RUN npm run package
FROM node:20.12.0-buster-slim

View File

@@ -6,6 +6,7 @@
"OFFICIAL": __MEMPOOL_OFFICIAL__,
"HTTP_PORT": __MEMPOOL_HTTP_PORT__,
"SPAWN_CLUSTER_PROCS": __MEMPOOL_SPAWN_CLUSTER_PROCS__,
"UNIX_SOCKET_PATH": "__MEMPOOL_UNIX_SOCKET_PATH__",
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
"POLL_RATE_MS": __MEMPOOL_POLL_RATE_MS__,
"CACHE_DIR": "__MEMPOOL_CACHE_DIR__",
@@ -93,10 +94,6 @@
"ENABLED": __STATISTICS_ENABLED__,
"TX_PER_SECOND_SAMPLE_PERIOD": __STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD__
},
"BISQ": {
"ENABLED": __BISQ_ENABLED__,
"DATA_PATH": "__BISQ_DATA_PATH__"
},
"LIGHTNING": {
"ENABLED": __LIGHTNING_ENABLED__,
"BACKEND": "__LIGHTNING_BACKEND__",
@@ -128,9 +125,7 @@
"MEMPOOL_API": "__EXTERNAL_DATA_SERVER_MEMPOOL_API__",
"MEMPOOL_ONION": "__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__",
"LIQUID_API": "__EXTERNAL_DATA_SERVER_LIQUID_API__",
"LIQUID_ONION": "__EXTERNAL_DATA_SERVER_LIQUID_ONION__",
"BISQ_URL": "__EXTERNAL_DATA_SERVER_BISQ_URL__",
"BISQ_ONION": "__EXTERNAL_DATA_SERVER_BISQ_ONION__"
"LIQUID_ONION": "__EXTERNAL_DATA_SERVER_LIQUID_ONION__"
},
"MAXMIND": {
"ENABLED": __MAXMIND_ENABLED__,
@@ -155,6 +150,7 @@
},
"FIAT_PRICE": {
"ENABLED": __FIAT_PRICE_ENABLED__,
"PAID": __FIAT_PRICE_PAID__,
"API_KEY": "__FIAT_PRICE_API_KEY__"
}
}

View File

@@ -7,6 +7,7 @@ __MEMPOOL_ENABLED__=${MEMPOOL_ENABLED:=true}
__MEMPOOL_OFFICIAL__=${MEMPOOL_OFFICIAL:=false}
__MEMPOOL_HTTP_PORT__=${BACKEND_HTTP_PORT:=8999}
__MEMPOOL_SPAWN_CLUSTER_PROCS__=${MEMPOOL_SPAWN_CLUSTER_PROCS:=0}
__MEMPOOL_UNIX_SOCKET_PATH__=${MEMPOOL_UNIX_SOCKET_PATH:=""}
__MEMPOOL_API_URL_PREFIX__=${MEMPOOL_API_URL_PREFIX:=/api/v1/}
__MEMPOOL_POLL_RATE_MS__=${MEMPOOL_POLL_RATE_MS:=2000}
__MEMPOOL_CACHE_DIR__=${MEMPOOL_CACHE_DIR:=./cache}
@@ -94,10 +95,6 @@ __SYSLOG_FACILITY__=${SYSLOG_FACILITY:=local7}
__STATISTICS_ENABLED__=${STATISTICS_ENABLED:=true}
__STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD__=${STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD:=150}
# BISQ
__BISQ_ENABLED__=${BISQ_ENABLED:=false}
__BISQ_DATA_PATH__=${BISQ_DATA_PATH:=/bisq/statsnode-data/btc_mainnet/db}
# SOCKS5PROXY
__SOCKS5PROXY_ENABLED__=${SOCKS5PROXY_ENABLED:=false}
__SOCKS5PROXY_USE_ONION__=${SOCKS5PROXY_USE_ONION:=true}
@@ -111,8 +108,6 @@ __EXTERNAL_DATA_SERVER_MEMPOOL_API__=${EXTERNAL_DATA_SERVER_MEMPOOL_API:=https:/
__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__=${EXTERNAL_DATA_SERVER_MEMPOOL_ONION:=http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1}
__EXTERNAL_DATA_SERVER_LIQUID_API__=${EXTERNAL_DATA_SERVER_LIQUID_API:=https://liquid.network/api/v1}
__EXTERNAL_DATA_SERVER_LIQUID_ONION__=${EXTERNAL_DATA_SERVER_LIQUID_ONION:=http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1}
__EXTERNAL_DATA_SERVER_BISQ_URL__=${EXTERNAL_DATA_SERVER_BISQ_URL:=https://bisq.markets/api}
__EXTERNAL_DATA_SERVER_BISQ_ONION__=${EXTERNAL_DATA_SERVER_BISQ_ONION:=http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api}
# LIGHTNING
__LIGHTNING_ENABLED__=${LIGHTNING_ENABLED:=false}
@@ -140,8 +135,8 @@ __MAXMIND_GEOLITE2_ASN__=${MAXMIND_GEOLITE2_ASN:="/backend/GeoIP/GeoLite2-ASN.mm
__MAXMIND_GEOIP2_ISP__=${MAXMIND_GEOIP2_ISP:=""}
# REPLICATION
__REPLICATION_ENABLED__=${REPLICATION_ENABLED:=true}
__REPLICATION_AUDIT__=${REPLICATION_AUDIT:=true}
__REPLICATION_ENABLED__=${REPLICATION_ENABLED:=false}
__REPLICATION_AUDIT__=${REPLICATION_AUDIT:=false}
__REPLICATION_AUDIT_START_HEIGHT__=${REPLICATION_AUDIT_START_HEIGHT:=774000}
__REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
@@ -156,6 +151,7 @@ __REDIS_BATCH_QUERY_BASE_SIZE__=${REDIS_BATCH_QUERY_BASE_SIZE:=5000}
# FIAT_PRICE
__FIAT_PRICE_ENABLED__=${FIAT_PRICE_ENABLED:=true}
__FIAT_PRICE_PAID__=${FIAT_PRICE_PAID:=false}
__FIAT_PRICE_API_KEY__=${FIAT_PRICE_API_KEY:=""}
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
@@ -166,6 +162,7 @@ sed -i "s!__MEMPOOL_ENABLED__!${__MEMPOOL_ENABLED__}!g" mempool-config.json
sed -i "s!__MEMPOOL_OFFICIAL__!${__MEMPOOL_OFFICIAL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_HTTP_PORT__!${__MEMPOOL_HTTP_PORT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_SPAWN_CLUSTER_PROCS__!${__MEMPOOL_SPAWN_CLUSTER_PROCS__}!g" mempool-config.json
sed -i "s!__MEMPOOL_UNIX_SOCKET_PATH__!${__MEMPOOL_UNIX_SOCKET_PATH__}!g" mempool-config.json
sed -i "s!__MEMPOOL_API_URL_PREFIX__!${__MEMPOOL_API_URL_PREFIX__}!g" mempool-config.json
sed -i "s!__MEMPOOL_POLL_RATE_MS__!${__MEMPOOL_POLL_RATE_MS__}!g" mempool-config.json
sed -i "s!__MEMPOOL_CACHE_DIR__!${__MEMPOOL_CACHE_DIR__}!g" mempool-config.json
@@ -246,9 +243,6 @@ sed -i "s!__SYSLOG_FACILITY__!${__SYSLOG_FACILITY__}!g" mempool-config.json
sed -i "s!__STATISTICS_ENABLED__!${__STATISTICS_ENABLED__}!g" mempool-config.json
sed -i "s!__STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD__!${__STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD__}!g" mempool-config.json
sed -i "s!__BISQ_ENABLED__!${__BISQ_ENABLED__}!g" mempool-config.json
sed -i "s!__BISQ_DATA_PATH__!${__BISQ_DATA_PATH__}!g" mempool-config.json
sed -i "s!__SOCKS5PROXY_ENABLED__!${__SOCKS5PROXY_ENABLED__}!g" mempool-config.json
sed -i "s!__SOCKS5PROXY_USE_ONION__!${__SOCKS5PROXY_USE_ONION__}!g" mempool-config.json
sed -i "s!__SOCKS5PROXY_HOST__!${__SOCKS5PROXY_HOST__}!g" mempool-config.json
@@ -260,8 +254,6 @@ sed -i "s!__EXTERNAL_DATA_SERVER_MEMPOOL_API__!${__EXTERNAL_DATA_SERVER_MEMPOOL_
sed -i "s!__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__!${__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__}!g" mempool-config.json
sed -i "s!__EXTERNAL_DATA_SERVER_LIQUID_API__!${__EXTERNAL_DATA_SERVER_LIQUID_API__}!g" mempool-config.json
sed -i "s!__EXTERNAL_DATA_SERVER_LIQUID_ONION__!${__EXTERNAL_DATA_SERVER_LIQUID_ONION__}!g" mempool-config.json
sed -i "s!__EXTERNAL_DATA_SERVER_BISQ_URL__!${__EXTERNAL_DATA_SERVER_BISQ_URL__}!g" mempool-config.json
sed -i "s!__EXTERNAL_DATA_SERVER_BISQ_ONION__!${__EXTERNAL_DATA_SERVER_BISQ_ONION__}!g" mempool-config.json
# LIGHTNING
sed -i "s!__LIGHTNING_ENABLED__!${__LIGHTNING_ENABLED__}!g" mempool-config.json
@@ -305,6 +297,7 @@ sed -i "s!__REDIS_BATCH_QUERY_BASE_SIZE__!${__REDIS_BATCH_QUERY_BASE_SIZE__}!g"
# FIAT_PRICE
sed -i "s!__FIAT_PRICE_ENABLED__!${__FIAT_PRICE_ENABLED__}!g" mempool-config.json
sed -i "s!__FIAT_PRICE_PAID__!${__FIAT_PRICE_PAID__}!g" mempool-config.json
sed -i "s!__FIAT_PRICE_API_KEY__!${__FIAT_PRICE_API_KEY__}!g" mempool-config.json
node /backend/package/index.js

View File

@@ -20,8 +20,6 @@ __TESTNET_ENABLED__=${TESTNET_ENABLED:=false}
__SIGNET_ENABLED__=${SIGNET_ENABLED:=false}
__LIQUID_ENABLED__=${LIQUID_ENABLED:=false}
__LIQUID_TESTNET_ENABLED__=${LIQUID_TESTNET_ENABLED:=false}
__BISQ_ENABLED__=${BISQ_ENABLED:=false}
__BISQ_SEPARATE_BACKEND__=${BISQ_SEPARATE_BACKEND:=false}
__ITEMS_PER_PAGE__=${ITEMS_PER_PAGE:=10}
__KEEP_BLOCKS_AMOUNT__=${KEEP_BLOCKS_AMOUNT:=8}
__NGINX_PROTOCOL__=${NGINX_PROTOCOL:=http}
@@ -32,7 +30,6 @@ __MEMPOOL_BLOCKS_AMOUNT__=${MEMPOOL_BLOCKS_AMOUNT:=8}
__BASE_MODULE__=${BASE_MODULE:=mempool}
__MEMPOOL_WEBSITE_URL__=${MEMPOOL_WEBSITE_URL:=https://mempool.space}
__LIQUID_WEBSITE_URL__=${LIQUID_WEBSITE_URL:=https://liquid.network}
__BISQ_WEBSITE_URL__=${BISQ_WEBSITE_URL:=https://bisq.markets}
__MINING_DASHBOARD__=${MINING_DASHBOARD:=true}
__LIGHTNING__=${LIGHTNING:=false}
__AUDIT__=${AUDIT:=false}
@@ -40,6 +37,7 @@ __MAINNET_BLOCK_AUDIT_START_HEIGHT__=${MAINNET_BLOCK_AUDIT_START_HEIGHT:=0}
__TESTNET_BLOCK_AUDIT_START_HEIGHT__=${TESTNET_BLOCK_AUDIT_START_HEIGHT:=0}
__SIGNET_BLOCK_AUDIT_START_HEIGHT__=${SIGNET_BLOCK_AUDIT_START_HEIGHT:=0}
__ACCELERATOR__=${ACCELERATOR:=false}
__PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false}
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
__ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false}
@@ -48,8 +46,6 @@ export __TESTNET_ENABLED__
export __SIGNET_ENABLED__
export __LIQUID_ENABLED__
export __LIQUID_TESTNET_ENABLED__
export __BISQ_ENABLED__
export __BISQ_SEPARATE_BACKEND__
export __ITEMS_PER_PAGE__
export __KEEP_BLOCKS_AMOUNT__
export __NGINX_PROTOCOL__
@@ -60,7 +56,6 @@ export __MEMPOOL_BLOCKS_AMOUNT__
export __BASE_MODULE__
export __MEMPOOL_WEBSITE_URL__
export __LIQUID_WEBSITE_URL__
export __BISQ_WEBSITE_URL__
export __MINING_DASHBOARD__
export __LIGHTNING__
export __AUDIT__
@@ -68,6 +63,7 @@ export __MAINNET_BLOCK_AUDIT_START_HEIGHT__
export __TESTNET_BLOCK_AUDIT_START_HEIGHT__
export __SIGNET_BLOCK_AUDIT_START_HEIGHT__
export __ACCELERATOR__
export __PUBLIC_ACCELERATIONS__
export __HISTORICAL_PRICE__
export __ADDITIONAL_CURRENCIES__

View File

@@ -22,14 +22,13 @@ cd mempool/frontend
### 2. Specify Website
The same frontend codebase is used for https://mempool.space, https://liquid.network and https://bisq.markets.
The same frontend codebase is used for https://mempool.space and https://liquid.network.
Configure the frontend for the site you want by running the corresponding command:
```
$ npm run config:defaults:mempool
$ npm run config:defaults:liquid
$ npm run config:defaults:bisq
```
### 3. Run the Frontend

View File

@@ -170,6 +170,16 @@
],
"styles": [
"src/styles.scss",
{
"input": "src/theme-contrast.scss",
"bundleName": "contrast",
"inject": false
},
{
"input": "src/theme-wiz.scss",
"bundleName": "wiz",
"inject": false
},
"node_modules/@fortawesome/fontawesome-svg-core/styles.css"
],
"vendorChunk": true,

View File

@@ -1,164 +0,0 @@
describe('Bisq', () => {
const baseModule = Cypress.env('BASE_MODULE');
const basePath = '';
beforeEach(() => {
cy.intercept('/sockjs-node/info*').as('socket');
cy.intercept('/bisq/api/markets/hloc?market=btc_usd&interval=day').as('hloc');
cy.intercept('/bisq/api/markets/ticker').as('ticker');
cy.intercept('/bisq/api/markets/markets').as('markets');
cy.intercept('/bisq/api/markets/volumes/7d').as('7d');
cy.intercept('/bisq/api/markets/trades?market=all').as('trades');
cy.intercept('/bisq/api/txs/*/*').as('txs');
cy.intercept('/bisq/api/blocks/*/*').as('blocks');
cy.intercept('/bisq/api/stats').as('stats');
});
if (baseModule === 'bisq') {
it('loads the dashboard', () => {
cy.visit(`${basePath}`);
cy.waitForSkeletonGone();
});
describe('transactions', () => {
it('loads the transactions screen', () => {
cy.visit(`${basePath}`);
cy.waitForSkeletonGone();
cy.get('#btn-transactions').click().then(() => {
cy.get('.table > tr').should('have.length', 50);
});
});
const filters = [
'Asset listing fee', 'Blind vote', 'Compensation request',
'Genesis', 'Irregular', 'Lockup', 'Pay trade fee', 'Proof of burn',
'Proposal', 'Reimbursement request', 'Transfer BSQ', 'Unlock', 'Vote reveal'
];
filters.forEach((filter) => {
it.only(`filters the transaction screen by ${filter}`, () => {
cy.visit(`${basePath}/transactions`);
cy.wait('@txs');
cy.waitForSkeletonGone();
cy.get('#filter').click();
cy.contains(filter).find('input').click();
cy.wait('@txs');
cy.wait(500);
cy.get('td:nth-of-type(2)').each(($td) => {
expect($td.text().trim()).to.eq(filter);
});
});
});
it('filters using multiple criteria', () => {
const filters = ['Proposal', 'Lockup', 'Unlock'];
cy.visit(`${basePath}/transactions`);
cy.waitForSkeletonGone();
cy.get('#filter').click();
filters.forEach((filter) => {
cy.contains(filter).find('input').click();
//TODO: change this waiter
cy.wait(1500);
});
cy.get('td:nth-of-type(2)').each(($td) => {
const regex = new RegExp(`${filters.join('|')}`, 'g');
expect($td.text().trim()).to.match(regex);
});
});
const transactions = [
{ type: 'Asset listing fee', txid: '3548aa0c002b015ea700072b7d7d76d45d4f10a3573804d0d2f624c0bb255b6b' },
{ type: 'Blind vote', txid: 'f8fabb95efa1bb81325e4c961b9fc7e3508a9b9ecd4eddf1400e58867eff8d92' },
{ type: 'Compensation request', txid: 'a8cdc65fe6bb8730f5f89f99f779d0469b0a493e1ae570e20eb7afda696a18a9' },
{ type: 'Genesis', txid: '4b5417ec5ab6112bedf539c3b4f5a806ed539542d8b717e1c4470aa3180edce5' },
{ type: 'Irregular', txid: '90b06684a517388fec2237e2362a29810dc82f0e13e019c84747ec27051e6c53' },
{ type: 'Lockup', txid: '365425b3b7487229e2ba598fb8f2a9e359e3351620383e5018548649a28b78c4' },
{ type: 'Pay trade fee', txid: 'a66b30e9777e16572ab36723539df8f45bd5d8130d810b2c3d75b8c02a191eaf' },
{ type: 'Proof of burn', txid: '8325ccb87065fb9243ed9ff1cbb431fc2ac5060a60433bcde474ccbd97b76dcb' },
{ type: 'Proposal', txid: '34e2a20f045c82fbcf7cb191b42dea6fba45641777e1751ffb29d3981c4bf413' },
{ type: 'Reimbursement request', txid: '04c16c79ca6b9ec9978880024b0d0ad3100020f33286b63c85ca7b1a319421ae' },
{ type: 'Transfer BSQ', txid: '64500bd9220675ad30d5ace27de95a341a498d7eda08162ee0ce7feb8c56cb14' },
{ type: 'Unlock', txid: '5a756841bbb11137d15b0082a3fcadbe102791f41a95d661d3bd0c5ad0b3b1a3' },
{ type: 'Vote reveal', txid: 'bd7daae1d4af8837db5e47d7bd9d8b9f83dcfd35d112f85e90728b9be45191f7' }
];
transactions.forEach((transaction) => {
it(`loads a "${transaction.type}" transaction`, () => {
cy.visit(`${basePath}/tx/${transaction.txid}`);
cy.waitForSkeletonGone();
});
});
});
describe('blocks', () => {
it('loads the blocks screen', () => {
cy.visit(`${basePath}`);
cy.waitForSkeletonGone();
cy.get('#btn-blocks').click().then(() => {
cy.wait('@blocks');
cy.get('tbody tr').should('have.length', 10);
});
});
it('loads a specific block', () => {
cy.visit(`${basePath}/block/0000000000000000000137ef33faa63bc6e809ab30932cf77d454fb36d2bd83a`);
cy.waitForSkeletonGone();
});
});
describe('markets', () => {
it('loads the markets screen', () => {
cy.visit(`${basePath}/markets`);
cy.waitForSkeletonGone();
});
it('loads a specific market', () => {
cy.visit(`${basePath}/market/btc_eur`);
cy.waitForSkeletonGone();
//Buy Offers
cy.get('.row > :nth-child(1) td').should('have.length.at.least', 1);
//Sell offers
cy.get('.row > :nth-child(1) td').should('have.length.at.least', 1);
//Trades
cy.get('app-bisq-trades > .table-container td').should('have.length.at.least', 1);
});
});
it('loads the stats screen', () => {
cy.visit(`${basePath}`);
cy.waitForSkeletonGone();
cy.get('#btn-stats').click().then(() => {
cy.wait('@stats');
});
});
it('loads the api screen', () => {
cy.visit(`${basePath}`);
cy.waitForSkeletonGone();
cy.get('#btn-docs').click().then(() => {
cy.get('.section-header').should('have.length.at.least', 1);
cy.get('.endpoint-container').should('have.length.at.least', 1);
});
});
it('shows blocks pagination with 5 pages (desktop)', () => {
cy.viewport(760, 800);
cy.visit(`${basePath}/blocks`);
cy.waitForSkeletonGone();
cy.get('tbody tr').should('have.length', 10);
// 5 pages + 4 buttons = 9 buttons
cy.get('.pagination-container ul.pagination').first().children().should('have.length', 9);
});
it('shows blocks pagination with 3 pages (mobile)', () => {
cy.viewport(669, 800);
cy.visit(`${basePath}/blocks`);
cy.waitForSkeletonGone();
cy.get('tbody tr').should('have.length', 10);
// 3 pages + 4 buttons = 7 buttons
cy.get('.pagination-container ul.pagination').first().children().should('have.length', 7);
});
} else {
it.skip(`Tests cannot be run on the selected BASE_MODULE ${baseModule}`);
}
});

View File

@@ -1,4 +1,4 @@
describe.skip('Liquid', () => {
describe('Liquid', () => {
const baseModule = Cypress.env('BASE_MODULE');
const basePath = '';
@@ -23,6 +23,13 @@ describe.skip('Liquid', () => {
cy.get('#mempool-block-0 > .blockLink').should('exist');
});
it('load first mempool block after skeleton loads', () => {
cy.visit(`${basePath}`);
cy.waitForSkeletonGone();
cy.get('#mempool-block-0 > .blockLink').click();
cy.waitForSkeletonGone();
});
it('loads the dashboard', () => {
cy.visit(`${basePath}`);
cy.waitForSkeletonGone();
@@ -84,10 +91,11 @@ describe.skip('Liquid', () => {
cy.waitForSkeletonGone();
//TODO: Change to an element id so we don't assert on a string
cy.get('.table-tx-vin').should('contain', 'Peg-in');
cy.get('.table-tx-vin a').click().then(() => {
//Remove the target=_blank attribute so the new url opens in the same tab
cy.get('.table-tx-vin a').invoke('removeAttr', 'target').click().then(() => {
cy.waitForSkeletonGone();
if (baseModule === 'liquid') {
cy.url().should('eq', 'https://mempool.space/tx/f148c0d854db4174ea420655235f910543f0ec3680566dcfdf84fb0a1697b592');
cy.url().should('eq', 'https://mempool.space/tx/f148c0d854db4174ea420655235f910543f0ec3680566dcfdf84fb0a1697b592#vout=0');
} else {
//TODO: Use an environment variable to get the hostname
cy.url().should('eq', 'http://localhost:4200/tx/f148c0d854db4174ea420655235f910543f0ec3680566dcfdf84fb0a1697b592');
@@ -98,7 +106,8 @@ describe.skip('Liquid', () => {
it('loads peg out addresses', () => {
cy.visit(`${basePath}/tx/ecf6eba04ffb3946faa172343c87162df76f1a57b07b0d6dc6ad956b13376dc8`);
cy.waitForSkeletonGone();
cy.get('.table-tx-vout a').first().click().then(() => {
//Remove the target=_blank attribute so the new url opens in the same tab
cy.get('.table-tx-vout a').first().invoke('removeAttr', 'target').click().then(() => {
cy.waitForSkeletonGone();
if (baseModule === 'liquid') {
cy.url().should('eq', 'https://mempool.space/address/1BxoGcMg14oaH3CwHD2hF4gU9VcfgX5yoR');

View File

@@ -1,4 +1,4 @@
describe.skip('Liquid Testnet', () => {
describe('Liquid Testnet', () => {
const baseModule = Cypress.env('BASE_MODULE');
const basePath = '/testnet';
@@ -28,6 +28,17 @@ describe.skip('Liquid Testnet', () => {
cy.waitForSkeletonGone();
});
it.skip('loads the dashboard with no scrollbars on mobile', () => {
cy.viewport('iphone-xr');
cy.visit(`${basePath}`);
cy.waitForSkeletonGone();
cy.window().then(window => {
const htmlWidth = Cypress.$('html')[0].scrollWidth;
const scrollBarWidth = window.innerWidth - htmlWidth;
expect(scrollBarWidth).to.be.eq(0); //check for no horizontal scrollbar
});
});
it('loads the blocks page', () => {
cy.visit(`${basePath}`)
cy.get('#btn-blocks');
@@ -57,17 +68,14 @@ describe.skip('Liquid Testnet', () => {
cy.get('.tv-only').should('not.exist');
});
it.skip('renders unconfidential addresses correctly on mobile', () => {
cy.viewport('iphone-6');
cy.visit(`${basePath}/address/__TODO__`);
it.skip('renders unconfidential transactions correctly on mobile', () => {
cy.viewport('iphone-xr');
cy.visit(`${basePath}/tx/b119f338878416781dc285b94c0de52826341dea43566e4de4740d3ebfd1f6dc#blinded=99707,144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49,1377e4ec8eb0c89296e14ffca57e377f4b51ad8f1c881e43364434d8430dbfda,cdd6caae4c3452586cfcb107478dd2b7acaa5f82714a6a966578255e857eee60`);
cy.waitForSkeletonGone();
//TODO: Add proper IDs for these selectors
const firstRowSelector = '.container-xl > :nth-child(3) > div > :nth-child(1) > .table > tbody';
const thirdRowSelector = '.container-xl > :nth-child(3) > div > :nth-child(3)';
cy.get(firstRowSelector).invoke('css', 'width').then(firstRowWidth => {
cy.get(thirdRowSelector).invoke('css', 'width').then(thirdRowWidth => {
expect(parseInt(firstRowWidth)).to.be.lessThan(parseInt(thirdRowWidth));
});
cy.window().then(window => {
const htmlWidth = Cypress.$('html')[0].scrollWidth;
const scrollBarWidth = window.innerWidth - htmlWidth;
expect(scrollBarWidth).to.be.eq(0); //check for no horizontal scrollbar
});
});

View File

@@ -67,7 +67,7 @@ describe('Mainnet', () => {
cy.get('[id^="bitcoin-block-"]').should('have.length', 22);
cy.get('.footer').should('be.visible');
cy.get('.row > :nth-child(1)').invoke('text').then((text) => {
expect(text).to.match(/Incoming transactions.* vB\/s/);
expect(text).to.match(/Incoming Transactions.* vB\/s/);
});
cy.get('.row > :nth-child(2)').invoke('text').then((text) => {
expect(text).to.match(/Unconfirmed:(.*)/);

View File

@@ -72,13 +72,11 @@ Cypress.Commands.add('mockMempoolSocket', () => {
mockWebSocket();
});
Cypress.Commands.add('changeNetwork', (network: "testnet" | "signet" | "liquid" | "bisq" | "mainnet") => {
Cypress.Commands.add('changeNetwork', (network: "testnet" | "signet" | "liquid" | "mainnet") => {
cy.get('.dropdown-toggle').click().then(() => {
cy.get(`a.${network}`).click().then(() => {
cy.waitForPageIdle();
if (network !== 'bisq') {
cy.waitForSkeletonGone();
}
cy.waitForSkeletonGone();
});
});
});

View File

@@ -5,6 +5,6 @@ declare namespace Cypress {
waitForSkeletonGone(): Chainable<any>
waitForPageIdle(): Chainable<any>
mockMempoolSocket(): Chainable<any>
changeNetwork(network: "testnet"|"signet"|"liquid"|"bisq"|"mainnet"): Chainable<any>
changeNetwork(network: "testnet"|"signet"|"liquid"|"mainnet"): Chainable<any>
}
}

View File

@@ -3,8 +3,6 @@
"SIGNET_ENABLED": false,
"LIQUID_ENABLED": false,
"LIQUID_TESTNET_ENABLED": false,
"BISQ_ENABLED": false,
"BISQ_SEPARATE_BACKEND": false,
"ITEMS_PER_PAGE": 10,
"KEEP_BLOCKS_AMOUNT": 8,
"NGINX_PROTOCOL": "http",
@@ -15,7 +13,6 @@
"BASE_MODULE": "mempool",
"MEMPOOL_WEBSITE_URL": "https://mempool.space",
"LIQUID_WEBSITE_URL": "https://liquid.network",
"BISQ_WEBSITE_URL": "https://bisq.markets",
"MINING_DASHBOARD": true,
"AUDIT": false,
"MAINNET_BLOCK_AUDIT_START_HEIGHT": 0,
@@ -24,5 +21,6 @@
"LIGHTNING": false,
"HISTORICAL_PRICE": true,
"ADDITIONAL_CURRENCIES": false,
"ACCELERATOR": false
"ACCELERATOR": false,
"PUBLIC_ACCELERATIONS": false
}

View File

@@ -9860,9 +9860,9 @@
"integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw=="
},
"node_modules/express": {
"version": "4.19.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.1.tgz",
"integrity": "sha512-K4w1/Bp7y8iSiVObmCrtq8Cs79XjJc/RU2YYkZQ7wpUu5ZyZ7MtPHkqoMz4pf+mgXfNvo2qft8D9OnrH2ABk9w==",
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -25526,9 +25526,9 @@
"integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw=="
},
"express": {
"version": "4.19.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.1.tgz",
"integrity": "sha512-K4w1/Bp7y8iSiVObmCrtq8Cs79XjJc/RU2YYkZQ7wpUu5ZyZ7MtPHkqoMz4pf+mgXfNvo2qft8D9OnrH2ABk9w==",
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
"requires": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",

View File

@@ -34,13 +34,12 @@
"start:local-prod": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-prod",
"start:local-staging": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-staging",
"start:mixed": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c mixed",
"build": "npm run generate-config && npm run ng -- build --configuration production --localize && npm run sync-assets && npm run build-mempool.js",
"build": "npm run generate-config && npm run ng -- build --configuration production --localize && npm run sync-assets-dev && npm run sync-assets && npm run build-mempool.js",
"sync-assets": "rsync -av ./src/resources ./dist/mempool/browser && node sync-assets.js 'dist/mempool/browser/resources/'",
"sync-assets-dev": "node sync-assets.js 'src/resources/'",
"generate-config": "node generate-config.js",
"build-mempool.js": "npm run build-mempool-js && npm run build-mempool-liquid-js && npm run build-mempool-bisq-js",
"build-mempool.js": "npm run build-mempool-js && npm run build-mempool-liquid-js",
"build-mempool-js": "browserify -p tinyify ./node_modules/@mempool/mempool.js/lib/index.js --standalone mempoolJS > ./dist/mempool/browser/en-US/mempool.js",
"build-mempool-bisq-js": "browserify -p tinyify ./node_modules/@mempool/mempool.js/lib/index-bisq.js --standalone bisqJS > ./dist/mempool/browser/en-US/bisq.js",
"build-mempool-liquid-js": "browserify -p tinyify ./node_modules/@mempool/mempool.js/lib/index-liquid.js --standalone liquidJS > ./dist/mempool/browser/en-US/liquid.js",
"test": "npm run ng -- test",
"lint": "./node_modules/.bin/eslint . --ext .ts",
@@ -51,17 +50,16 @@
"dev:ssr": "npm run generate-config && ng run mempool:serve-ssr",
"serve:ssr": "npm run generate-config && node server.run.js",
"build:ssr": "npm run build && ng run mempool:server:production && ./node_modules/typescript/bin/tsc server.run.ts",
"config:defaults:mempool": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=mempool BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
"config:defaults:liquid": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=liquid BLOCK_WEIGHT_UNITS=300000 && npm run generate-config",
"config:defaults:bisq": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=bisq BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
"config:defaults:mempool": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=mempool BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
"config:defaults:liquid": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=liquid BLOCK_WEIGHT_UNITS=300000 && npm run generate-config",
"prerender": "npm run ng -- run mempool:prerender",
"cypress:open": "cypress open",
"cypress:run": "cypress run",
"cypress:run:record": "cypress run --record",
"cypress:open:ci": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:open",
"cypress:run:ci": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:run:record",
"cypress:open:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:open",
"cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record"
"cypress:open:ci": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:open",
"cypress:run:ci": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:run:record",
"cypress:open:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:open",
"cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record"
},
"dependencies": {
"@angular-devkit/build-angular": "^17.3.1",

View File

@@ -22,7 +22,6 @@ PROXY_CONFIG = [
{
context: ['*',
'/api/**', '!/api/v1/ws',
'!/bisq', '!/bisq/**', '!/bisq/',
'!/liquid', '!/liquid/**', '!/liquid/',
'!/liquidtestnet', '!/liquidtestnet/**', '!/liquidtestnet/',
'/testnet/api/**', '/signet/api/**'
@@ -39,16 +38,6 @@ PROXY_CONFIG = [
secure: false,
changeOrigin: true,
},
{
context: ['/api/bisq**', '/bisq/api/**'],
target: "https://bisq.markets",
pathRewrite: {
"^/api/bisq/": "/bisq/api"
},
ws: true,
secure: false,
changeOrigin: true
},
{
context: ['/api/liquid**', '/liquid/api/**'],
target: "https://liquid.network",

View File

@@ -67,40 +67,6 @@ if (configContent && configContent.BASE_MODULE === 'liquid') {
]);
}
if (configContent && configContent.BASE_MODULE === 'bisq') {
PROXY_CONFIG.push(...[
{
context: ['/bisq/api/v1/ws'],
target: `http://127.0.0.1:8999`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
pathRewrite: {
"^/bisq": ""
},
},
{
context: ['/bisq/api/v1/**'],
target: `http://127.0.0.1:8999`,
secure: false,
changeOrigin: true,
proxyTimeout: 30000,
},
{
context: ['/bisq/api/**'],
target: `http://127.0.0.1:8999`,
secure: false,
changeOrigin: true,
proxyTimeout: 30000,
pathRewrite: {
"^/bisq/api/": "/api/v1/bisq/"
},
}
]);
}
PROXY_CONFIG.push(...[
{
context: ['/testnet/api/v1/lightning/**'],

View File

@@ -67,40 +67,6 @@ if (configContent && configContent.BASE_MODULE === 'liquid') {
]);
}
if (configContent && configContent.BASE_MODULE === 'bisq') {
PROXY_CONFIG.push(...[
{
context: ['/bisq/api/v1/ws'],
target: `http://localhost:8999`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
pathRewrite: {
"^/bisq": ""
},
},
{
context: ['/bisq/api/v1/**'],
target: `http://localhost:8999`,
secure: false,
changeOrigin: true,
proxyTimeout: 30000,
},
{
context: ['/bisq/api/**'],
target: `http://localhost:8999`,
secure: false,
changeOrigin: true,
proxyTimeout: 30000,
pathRewrite: {
"^/bisq/api/": "/api/v1/bisq/"
},
}
]);
}
PROXY_CONFIG.push(...[
{
context: ['/testnet/api/v1/lightning/**'],

View File

@@ -61,39 +61,6 @@ if (configContent && configContent.BASE_MODULE === 'liquid') {
]);
}
if (configContent && configContent.BASE_MODULE === 'bisq') {
PROXY_CONFIG.push(...[
{
context: ['/bisq/api/v1/ws'],
target: `http://localhost:8999`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
pathRewrite: {
"^/bisq": ""
},
},
{
context: ['/bisq/api/v1/**'],
target: `http://localhost:8999`,
secure: false,
changeOrigin: true,
proxyTimeout: 30000,
},
{
context: ['/bisq/api/**'],
target: `http://localhost:8999`,
secure: false,
changeOrigin: true,
proxyTimeout: 30000,
pathRewrite: {
"^/bisq/api/": "/api/v1/bisq/"
},
},
]);
}
PROXY_CONFIG.push(...[
{
context: ['/api/v1/services/**'],

View File

@@ -5,7 +5,6 @@ let PROXY_CONFIG = require('./proxy.conf');
PROXY_CONFIG.forEach(entry => {
entry.target = entry.target.replace("mempool.space", "mempool-staging.fra.mempool.space");
entry.target = entry.target.replace("liquid.network", "liquid-staging.fra.mempool.space");
entry.target = entry.target.replace("bisq.markets", "bisq-staging.fra.mempool.space");
});
module.exports = PROXY_CONFIG;

View File

@@ -7,6 +7,8 @@ import { MempoolBlockViewComponent } from './components/mempool-block-view/mempo
import { ClockComponent } from './components/clock/clock.component';
import { StatusViewComponent } from './components/status-view/status-view.component';
import { AddressGroupComponent } from './components/address-group/address-group.component';
import { TrackerComponent } from './components/tracker/tracker.component';
import { AccelerateCheckout } from './components/accelerate-checkout/accelerate-checkout.component';
const browserWindow = window || {};
// @ts-ignore
@@ -105,6 +107,10 @@ let routes: Routes = [
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
data: { preload: true },
},
{
path: 'tracker/:id',
component: TrackerComponent,
},
{
path: 'wallet',
children: [],
@@ -170,13 +176,6 @@ let routes: Routes = [
},
];
if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'bisq') {
routes = [{
path: '',
loadChildren: () => import('./bisq/bisq.module').then(m => m.BisqModule)
}];
}
if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
routes = [
{
@@ -270,7 +269,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
imports: [RouterModule.forRoot(routes, {
initialNavigation: 'enabledBlocking',
scrollPositionRestoration: 'enabled',
anchorScrolling: 'enabled',
anchorScrolling: 'disabled',
preloadingStrategy: AppPreloadingStrategy
})],
})

View File

@@ -1,4 +1,4 @@
export const mempoolFeeColors = [
export const defaultMempoolFeeColors = [
'557d00',
'5d7d01',
'637d02',
@@ -39,6 +39,47 @@ export const mempoolFeeColors = [
'ae005b',
];
export const contrastMempoolFeeColors = [
'0082e6',
'0984df',
'1285d9',
'1a87d2',
'2388cb',
'2c8ac5',
'358bbe',
'3e8db7',
'468eb0',
'4f90aa',
'5892a3',
'61939c',
'6a9596',
'72968f',
'7b9888',
'849982',
'8d9b7b',
'959c74',
'9e9e6e',
'a79f67',
'b0a160',
'b9a35a',
'c1a453',
'caa64c',
'd3a745',
'dca93f',
'e5aa38',
'edac31',
'f6ad2b',
'ffaf24',
'ffb01e',
'ffb118',
'ffb212',
'ffb30c',
'ffb406',
'ffb500',
'ffb600',
'ffb700',
];
export const chartColors = [
"#D81B60",
"#8E24AA",

View File

@@ -19,6 +19,7 @@ import { SharedModule } from './shared/shared.module';
import { StorageService } from './services/storage.service';
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
import { LanguageService } from './services/language.service';
import { ThemeService } from './services/theme.service';
import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe';
import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe';
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
@@ -38,6 +39,7 @@ const providers = [
StorageService,
EnterpriseService,
LanguageService,
ThemeService,
ShortenStringPipe,
FiatShortenerPipe,
FiatCurrencyPipe,

View File

@@ -1,108 +0,0 @@
<div class="container-xl">
<h1 i18n="shared.address">Address</h1>
<span class="address-link">
<app-truncate [text]="addressString" [lastChars]="8" [link]="['/address/' | relativeUrl, addressString]">
<app-clipboard [text]="addressString"></app-clipboard>
</app-truncate>
</span>
<br>
<div class="clearfix"></div>
<ng-template [ngIf]="!isLoadingAddress && !error">
<div class="box">
<div class="row">
<div class="col-md">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td i18n="address.total-received">Total received</td>
<td>{{ totalReceived / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
</tr>
<tr>
<td i18n="address.total-sent">Total sent</td>
<td>{{ totalSent / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
</tr>
<tr>
<td i18n="address.balance">Balance</td>
<td>{{ (totalReceived - totalSent) / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span> <span class="fiat"><app-bsq-amount [bsq]="totalReceived - totalSent" [forceFiat]="true" [green]="true"></app-bsq-amount></span></td>
</tr>
</tbody>
</table>
</div>
<div class="w-100 d-block d-md-none"></div>
<div class="col-md qrcode-col">
<div class="qr-wrapper">
<app-qrcode [data]="addressString"></app-qrcode>
</div>
</div>
</div>
</div>
<br>
<h2>
<ng-container *ngTemplateOutlet="transactions.length === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: transactions.length}"></ng-container>
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
</h2>
<ng-template ngFor let-tx [ngForOf]="transactions">
<div class="header-bg box" style="padding: 10px; margin-bottom: 10px;">
<a [routerLink]="['/tx/' | relativeUrl, tx.id]" [state]="{ data: tx }">
<span style="float: left;" class="d-block d-md-none">{{ tx.id | shortenString : 16 }}</span>
<span style="float: left;" class="d-none d-md-block">{{ tx.id }}</span>
</a>
<div class="float-right">
&lrm;{{ tx.time | date:'yyyy-MM-dd HH:mm' }}
</div>
<div class="clearfix"></div>
</div>
<app-bisq-transfers [tx]="tx" [showConfirmations]="true"></app-bisq-transfers>
<br>
</ng-template>
</ng-template>
<ng-template [ngIf]="isLoadingAddress && !error">
<div class="box">
<div class="row">
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</div>
<div class="w-100 d-block d-md-none"></div>
<div class="col">
</div>
</div>
</div>
</ng-template>
<ng-template [ngIf]="error">
<app-http-error [error]="error">
<span i18n="address.error.loading-address-data">Error loading address data.</span>
</app-http-error>
</ng-template>
</div>
<br>

View File

@@ -1,75 +0,0 @@
.qr-wrapper {
background-color: #FFF;
padding: 10px;
padding-bottom: 5px;
display: inline-block;
}
.qrcode-col {
text-align: center;
}
.qrcode-col > div {
margin: 20px auto 5px;
@media (min-width: 768px) {
text-align: center;
margin: auto;
}
}
.fiat {
display: block;
font-size: 13px;
@media (min-width: 768px) {
display: inline-block;
font-size: 14px;
margin-left: 10px;
}
}
.table {
tr td {
&:last-child {
text-align: right;
@media (min-width: 768px) {
text-align: left;
}
}
}
}
h1 {
margin: 0px;
padding: 0px;
@media (min-width: 576px) {
float: left;
margin-right: 10px;
}
}
.address-link {
line-height: 26px;
margin-left: 0px;
top: 14px;
position: relative;
display: flex;
flex-direction: row;
@media (min-width: 768px) {
line-height: 38px;
}
}
.row{
flex-direction: column;
@media (min-width: 576px) {
flex-direction: row;
}
}
@media (max-width: 767.98px) {
.mobile-bottomcol {
margin-top: 15px;
}
.details-table td:first-child {
white-space: pre-wrap;
}
}

View File

@@ -1,89 +0,0 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { SeoService } from '../../services/seo.service';
import { switchMap, filter, catchError } from 'rxjs/operators';
import { ParamMap, ActivatedRoute } from '@angular/router';
import { Subscription, of } from 'rxjs';
import { BisqTransaction } from '../bisq.interfaces';
import { BisqApiService } from '../bisq-api.service';
import { WebsocketService } from '../../services/websocket.service';
@Component({
selector: 'app-bisq-address',
templateUrl: './bisq-address.component.html',
styleUrls: ['./bisq-address.component.scss']
})
export class BisqAddressComponent implements OnInit, OnDestroy {
transactions: BisqTransaction[];
addressString: string;
isLoadingAddress = true;
error: any;
mainSubscription: Subscription;
totalReceived = 0;
totalSent = 0;
constructor(
private websocketService: WebsocketService,
private route: ActivatedRoute,
private seoService: SeoService,
private bisqApiService: BisqApiService,
) { }
ngOnInit() {
this.websocketService.want(['blocks']);
this.mainSubscription = this.route.paramMap
.pipe(
switchMap((params: ParamMap) => {
this.error = undefined;
this.isLoadingAddress = true;
this.transactions = null;
document.body.scrollTo(0, 0);
this.addressString = params.get('id') || '';
this.seoService.setTitle($localize`:@@bisq-address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
this.seoService.setDescription($localize`:@@meta.description.bisq.address:See current balance, pending transactions, and history of confirmed transactions for BSQ address ${this.addressString}:INTERPOLATION:.`);
return this.bisqApiService.getAddress$(this.addressString)
.pipe(
catchError((err) => {
this.isLoadingAddress = false;
this.error = err;
this.seoService.logSoft404();
console.log(err);
return of(null);
})
);
}),
filter((transactions) => transactions !== null)
)
.subscribe((transactions: BisqTransaction[]) => {
this.transactions = transactions;
this.updateChainStats();
this.isLoadingAddress = false;
},
(error) => {
console.log(error);
this.error = error;
this.seoService.logSoft404();
this.isLoadingAddress = false;
});
}
updateChainStats() {
const shortenedAddress = this.addressString.substr(1);
this.totalSent = this.transactions.reduce((acc, tx) =>
acc + tx.inputs
.filter((input) => input.address === shortenedAddress)
.reduce((a, input) => a + input.bsqAmount, 0), 0);
this.totalReceived = this.transactions.reduce((acc, tx) =>
acc + tx.outputs
.filter((output) => output.address === shortenedAddress)
.reduce((a, output) => a + output.bsqAmount, 0), 0);
}
ngOnDestroy() {
this.mainSubscription.unsubscribe();
}
}

View File

@@ -1,78 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpResponse, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { BisqTransaction, BisqBlock, BisqStats, MarketVolume, Trade, Markets, Tickers, Offers, Currencies, HighLowOpenClose, SummarizedInterval } from './bisq.interfaces';
const API_BASE_URL = '/bisq/api';
@Injectable({
providedIn: 'root'
})
export class BisqApiService {
apiBaseUrl: string;
constructor(
private httpClient: HttpClient,
) { }
getStats$(): Observable<BisqStats> {
return this.httpClient.get<BisqStats>(API_BASE_URL + '/stats');
}
getTransaction$(txId: string): Observable<BisqTransaction> {
return this.httpClient.get<BisqTransaction>(API_BASE_URL + '/tx/' + txId);
}
listTransactions$(start: number, length: number, types: string[]): Observable<HttpResponse<BisqTransaction[]>> {
let params = new HttpParams();
types.forEach((t: string) => {
params = params.append('types[]', t);
});
return this.httpClient.get<BisqTransaction[]>(API_BASE_URL + `/txs/${start}/${length}`, { params, observe: 'response' });
}
getBlock$(hash: string): Observable<BisqBlock> {
return this.httpClient.get<BisqBlock>(API_BASE_URL + '/block/' + hash);
}
listBlocks$(start: number, length: number): Observable<HttpResponse<BisqBlock[]>> {
return this.httpClient.get<BisqBlock[]>(API_BASE_URL + `/blocks/${start}/${length}`, { observe: 'response' });
}
getAddress$(address: string): Observable<BisqTransaction[]> {
return this.httpClient.get<BisqTransaction[]>(API_BASE_URL + '/address/' + address);
}
getMarkets$(): Observable<Markets> {
return this.httpClient.get<Markets>(API_BASE_URL + '/markets/markets');
}
getMarketsTicker$(): Observable<Tickers> {
return this.httpClient.get<Tickers>(API_BASE_URL + '/markets/ticker');
}
getMarketsCurrencies$(): Observable<Currencies> {
return this.httpClient.get<Currencies>(API_BASE_URL + '/markets/currencies');
}
getMarketsHloc$(market: string, interval: 'minute' | 'half_hour' | 'hour' | 'half_day' | 'day'
| 'week' | 'month' | 'year' | 'auto'): Observable<SummarizedInterval[]> {
return this.httpClient.get<SummarizedInterval[]>(API_BASE_URL + '/markets/hloc?market=' + market + '&interval=' + interval);
}
getMarketOffers$(market: string): Observable<Offers> {
return this.httpClient.get<Offers>(API_BASE_URL + '/markets/offers?market=' + market);
}
getMarketTrades$(market: string): Observable<Trade[]> {
return this.httpClient.get<Trade[]>(API_BASE_URL + '/markets/trades?market=' + market);
}
getMarketVolumesByTime$(period: string): Observable<HighLowOpenClose[]> {
return this.httpClient.get<HighLowOpenClose[]>(API_BASE_URL + '/markets/volumes/' + period);
}
getAllVolumesDay$(): Observable<MarketVolume[]> {
return this.httpClient.get<MarketVolume[]>(API_BASE_URL + '/markets/volumes?interval=week');
}
}

View File

@@ -1,114 +0,0 @@
<div class="container-xl">
<div class="title-block">
<h1><ng-template [ngIf]="blockHeight" i18n="shared.block-title">Block <ng-container *ngTemplateOutlet="blockTemplateContent"></ng-container></ng-template></h1>
</div>
<ng-template #blockTemplateContent><a [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockHeight }}</a></ng-template>
<div class="clearfix"></div>
<ng-template [ngIf]="!isLoading && !error">
<div class="box block-container">
<div class="row">
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width" i18n="block.hash">Hash</td>
<td><a [routerLink]="['/block/' | relativeUrl, block.hash]" title="{{ block.hash }}">{{ block.hash | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="block.hash"></app-clipboard></td>
</tr>
<tr>
<td i18n="block.timestamp">Timestamp</td>
<td>
&lrm;{{ block.time | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline">
<i class="symbol">(<app-time kind="since" [time]="block.time / 1000" [fastRender]="true"></app-time>)</i>
</div>
</td>
</tr>
</table>
</div>
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width" i18n="block.previous_hash|Transaction Previous Hash">Previous hash</td>
<td><a [routerLink]="['/block/' | relativeUrl, block.previousBlockHash]" title="{{ block.hash }}">{{ block.previousBlockHash | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="block.previousBlockHash"></app-clipboard></td>
</tr>
</table>
</div>
</div>
</div>
<div class="clearfix"></div>
<br>
<h2>
<ng-container *ngTemplateOutlet="block.txs.length === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.txs.length| number}"></ng-container>
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
</h2>
<ng-template ngFor let-tx [ngForOf]="block.txs">
<div class="header-bg box" style="padding: 10px; margin-bottom: 10px;">
<a [routerLink]="['/tx/' | relativeUrl, tx.id]" [state]="{ data: tx }">
<span style="float: left;" class="d-block d-md-none">{{ tx.id | shortenString : 16 }}</span>
<span style="float: left;" class="d-none d-md-block">{{ tx.id }}</span>
</a>
<div class="float-right">
&lrm;{{ tx.time | date:'yyyy-MM-dd HH:mm' }}
</div>
<div class="clearfix"></div>
</div>
<app-bisq-transfers [tx]="tx" [showConfirmations]="true"></app-bisq-transfers>
<br>
</ng-template>
</ng-template>
<ng-template [ngIf]="isLoading && !error">
<div class="box">
<div class="row">
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width" i18n="block.hash">Hash</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td i18n="block.timestamp">Timestamp</td>
<td><span class="skeleton-loader"></span></td>
</tr>
</table>
</div>
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width" i18n="block.previous_hash|Transaction Previous Hash">Previous hash</td>
<td><span class="skeleton-loader"></span></td>
</tr>
</table>
</div>
</div>
</div>
</ng-template>
<ng-template [ngIf]="error">
<div class="clearfix"></div>
<div class="text-center">
Error loading block
<br>
<i>{{ error.status }}: {{ error.statusText }}</i>
</div>
</ng-template>
</div>

View File

@@ -1,44 +0,0 @@
.td-width {
width: 140px;
@media (min-width: 768px) {
width: 175px;
}
}
h1 {
margin: 0px;
padding: 0px;
@media (min-width: 576px) {
float: left;
margin-right: 10px;
}
}
.row{
flex-direction: column;
@media (min-width: 768px) {
flex-direction: row;
}
}
.block-container {
.table {
tr td {
&:last-child {
text-align: right;
@media (min-width: 992px) {
text-align: left;
}
}
}
}
.fiat {
display: block;
font-size: 13px;
@media (min-width: 992px) {
display: inline-block;
font-size: 14px;
margin-left: 10px;
}
}
}

View File

@@ -1,106 +0,0 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { BisqBlock } from '../../bisq/bisq.interfaces';
import { Location } from '@angular/common';
import { BisqApiService } from '../bisq-api.service';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { Subscription, of } from 'rxjs';
import { switchMap, catchError } from 'rxjs/operators';
import { SeoService } from '../../services/seo.service';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { HttpErrorResponse } from '@angular/common/http';
import { WebsocketService } from '../../services/websocket.service';
@Component({
selector: 'app-bisq-block',
templateUrl: './bisq-block.component.html',
styleUrls: ['./bisq-block.component.scss']
})
export class BisqBlockComponent implements OnInit, OnDestroy {
block: BisqBlock;
subscription: Subscription;
blockHash = '';
blockHeight = 0;
isLoading = true;
error: HttpErrorResponse | null;
constructor(
private websocketService: WebsocketService,
private bisqApiService: BisqApiService,
private route: ActivatedRoute,
private seoService: SeoService,
private electrsApiService: ElectrsApiService,
private router: Router,
private location: Location,
) { }
ngOnInit(): void {
this.websocketService.want(['blocks']);
this.subscription = this.route.paramMap
.pipe(
switchMap((params: ParamMap) => {
const blockHash = params.get('id') || '';
document.body.scrollTo(0, 0);
this.isLoading = true;
this.error = null;
if (history.state.data && history.state.data.blockHeight) {
this.blockHeight = history.state.data.blockHeight;
}
if (history.state.data && history.state.data.block) {
this.blockHeight = history.state.data.block.height;
return of(history.state.data.block);
}
let isBlockHeight = false;
if (/^[0-9]+$/.test(blockHash)) {
isBlockHeight = true;
} else {
this.blockHash = blockHash;
}
if (isBlockHeight) {
return this.electrsApiService.getBlockHashFromHeight$(parseInt(blockHash, 10))
.pipe(
switchMap((hash) => {
if (!hash) {
return;
}
this.blockHash = hash;
this.location.replaceState(
this.router.createUrlTree(['/bisq/block/', hash]).toString()
);
this.seoService.updateCanonical(this.location.path());
return this.bisqApiService.getBlock$(this.blockHash)
.pipe(catchError(this.caughtHttpError.bind(this)));
}),
catchError(this.caughtHttpError.bind(this))
);
}
return this.bisqApiService.getBlock$(this.blockHash)
.pipe(catchError(this.caughtHttpError.bind(this)));
})
)
.subscribe((block: BisqBlock) => {
if (!block) {
this.seoService.logSoft404();
return;
}
this.isLoading = false;
this.blockHeight = block.height;
this.seoService.setTitle($localize`:@@bisq-block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.hash}:BLOCK_HASH:`);
this.seoService.setDescription($localize`:@@meta.description.bisq.block:See all BSQ transactions in Bitcoin block ${block.height}:BLOCK_HEIGHT: (block hash ${block.hash}:BLOCK_HASH:).`);
this.block = block;
});
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
caughtHttpError(err: HttpErrorResponse){
this.error = err;
this.seoService.logSoft404();
return of(null);
}
}

View File

@@ -1,40 +0,0 @@
<div class="container-xl" (window:resize)="onResize($event)">
<h1 style="float: left;" i18n="Bisq blocks header">BSQ Blocks</h1>
<br>
<div class="clearfix"></div>
<ng-container *ngIf="{ value: (blocks$ | async) } as blocks">
<div class="table-responsive-sm">
<table class="table table-borderless table-striped">
<thead>
<th style="width: 25%;" i18n="Bisq block height header">Height</th>
<th style="width: 25%;" i18n="Bisq block confirmed time header">Confirmed</th>
<th style="width: 25%;" i18n="Bisq block total BSQ tokens sent header">Total sent</th>
<th class="d-none d-md-block" style="width: 25%;" i18n="Bisq block transactions title">Transactions</th>
</thead>
<tbody *ngIf="blocks.value; else loadingTmpl">
<tr *ngFor="let block of blocks.value[0]; trackBy: trackByFn">
<td><a [routerLink]="['/block/' | relativeUrl, block.hash]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
<td><app-time kind="since" [time]="block.time / 1000" [fastRender]="true"></app-time></td>
<td>{{ calculateTotalOutput(block) / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
<td class="d-none d-md-block">{{ block.txs.length }}</td>
</tr>
</tbody>
</table>
</div>
<br>
<ngb-pagination *ngIf="blocks.value" class="pagination-container" [size]="paginationSize" [collectionSize]="blocks.value[1]" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="false"></ngb-pagination>
<div class="clearfix"></div>
<br>
</ng-container>
</div>
<ng-template #loadingTmpl>
<tr *ngFor="let i of loadingItems">
<td *ngFor="let j of [1, 2, 3, 4]"><span class="skeleton-loader"></span></td>
</tr>
</ng-template>

View File

@@ -1,6 +0,0 @@
.pagination-container {
float: none;
@media(min-width: 400px){
float: right;
}
}

View File

@@ -1,91 +0,0 @@
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { BisqApiService } from '../bisq-api.service';
import { switchMap, map, take, mergeMap, tap } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { BisqBlock, BisqOutput, BisqTransaction } from '../bisq.interfaces';
import { SeoService } from '../../services/seo.service';
import { ActivatedRoute, Router } from '@angular/router';
import { WebsocketService } from '../../services/websocket.service';
@Component({
selector: 'app-bisq-blocks',
templateUrl: './bisq-blocks.component.html',
styleUrls: ['./bisq-blocks.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BisqBlocksComponent implements OnInit {
blocks$: Observable<[BisqBlock[], number]>;
page = 1;
itemsPerPage: number;
contentSpace = window.innerHeight - (165 + 75);
fiveItemsPxSize = 250;
loadingItems: number[];
isLoading = true;
// @ts-ignore
paginationSize: 'sm' | 'lg' = 'md';
paginationMaxSize = 5;
constructor(
private websocketService: WebsocketService,
private bisqApiService: BisqApiService,
private seoService: SeoService,
private route: ActivatedRoute,
private router: Router,
) { }
ngOnInit(): void {
this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@8a7b4bd44c0ac71b2e72de0398b303257f7d2f54:Blocks`);
this.seoService.setDescription($localize`:@@meta.description.bisq.blocks:See a list of recent Bitcoin blocks with BSQ transactions, total BSQ sent per block, and more.`);
this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10);
this.loadingItems = Array(this.itemsPerPage);
if (document.body.clientWidth < 670) {
this.paginationSize = 'sm';
this.paginationMaxSize = 3;
}
this.blocks$ = this.route.queryParams
.pipe(
take(1),
tap((qp) => {
if (qp.page) {
this.page = parseInt(qp.page, 10);
}
}),
mergeMap(() => this.route.queryParams),
map((queryParams) => {
if (queryParams.page) {
const newPage = parseInt(queryParams.page, 10);
this.page = newPage;
return newPage;
} else {
this.page = 1;
}
return 1;
}),
switchMap((page) => this.bisqApiService.listBlocks$((page - 1) * this.itemsPerPage, this.itemsPerPage)),
map((response) => [response.body, parseInt(response.headers.get('x-total-count'), 10)]),
);
}
calculateTotalOutput(block: BisqBlock): number {
return block.txs.reduce((a: number, tx: BisqTransaction) =>
a + tx.outputs.reduce((acc: number, output: BisqOutput) => acc + output.bsqAmount, 0), 0
);
}
trackByFn(index: number) {
return index;
}
pageChange(page: number) {
this.router.navigate([], {
queryParams: { page: page },
queryParamsHandling: 'merge',
});
}
onResize(event: any) {
this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
}
}

View File

@@ -1,63 +0,0 @@
<div class="container-xl">
<h1 i18n="Bisq markets title">Bisq Trading Volume</h1>
<div id="volumeHolder">
<ng-template #loadingVolumes>
<div class="text-center loadingVolumes">
<div class="spinner-border text-light"></div>
</div>
</ng-template>
<ng-container *ngIf="volumes$ | async as volumes; else loadingVolumes">
<app-lightweight-charts-area [data]="volumes.data" [lineData]="volumes.linesData"></app-lightweight-charts-area>
</ng-container>
</div>
<br><br>
<div class="container-info">
<h1>
<ng-template [ngIf]="stateService.env.BASE_MODULE === 'bisq'" [ngIfElse]="nonOfficialMarkets" i18n="Bisq All Markets">Markets</ng-template>
<ng-template #nonOfficialMarkets i18n="Bisq Bitcoin Markets">Bitcoin Markets</ng-template>
</h1>
<ng-container *ngIf="{ value: (tickers$ | async) } as tickers">
<div class="table-container">
<table class="table table-borderless table-striped">
<thead>
<th><ng-container i18n>Currency</ng-container> <button [disabled]="(sort$ | async) === 'name'" class="btn btn-link btn-sm" (click)="sort('name')"><fa-icon [icon]="['fas', 'chevron-down']" [fixedWidth]="true"></fa-icon></button></th>
<th i18n>Price</th>
<th><ng-container i18n="Trading volume 7D">Volume (7d)</ng-container> <button [disabled]="(sort$ | async) === 'volumes'" class="btn btn-link btn-sm" (click)="sort('volumes')"><fa-icon [icon]="['fas', 'chevron-down']" [fixedWidth]="true"></fa-icon></button></th>
<th><ng-container i18n="Trades amount 7D">Trades (7d)</ng-container> <button [disabled]="(sort$ | async) === 'trades'" class="btn btn-link btn-sm" (click)="sort('trades')"><fa-icon [icon]="['fas', 'chevron-down']" [fixedWidth]="true"></fa-icon></button></th>
</thead>
<tbody *ngIf="tickers.value; else loadingTmpl">
<tr *ngFor="let ticker of tickers.value; trackBy: trackByFn;">
<td><a [routerLink]="['/market' | relativeUrl, ticker.pair_url]">{{ ticker.name }})</a></td>
<td>
<app-fiat *ngIf="ticker.market.rtype === 'crypto'; else fiat" [value]="ticker.last * 100000000"></app-fiat>
<ng-template #fiat>
<span class="green-color">{{ ticker.last | currency: ticker.market.rsymbol }}</span>
</ng-template>
</td>
<td>
<app-fiat [value]="ticker.volume?.volume"></app-fiat>
</td>
<td>{{ ticker.volume?.num_trades ? ticker.volume?.num_trades : 0 }}</td>
</tr>
</tbody>
</table>
</div>
<br><br>
<h2 i18n="Latest Trades header">Latest Trades</h2>
<app-bisq-trades [trades$]="trades$"></app-bisq-trades>
</ng-container>
</div>
</div>
<ng-template #loadingTmpl>
<tr *ngFor="let i of [1,2,3,4,5,6,7,8,9,10]">
<td *ngFor="let j of [1, 2, 3, 4]"><span class="skeleton-loader"></span></td>
</tr>
</ng-template>

View File

@@ -1,22 +0,0 @@
#volumeHolder {
height: 500px;
background-color: #000;
overflow: hidden;
display: flex;
justify-content: center;
}
.table {
max-width: 100%;
overflow: scroll;
}
.loadingVolumes {
position: relative;
top: 45%;
z-index: 100;
}
.container-info{
overflow-x: scroll;
}

View File

@@ -1,132 +0,0 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { Observable, combineLatest, BehaviorSubject, of } from 'rxjs';
import { map, share, switchMap } from 'rxjs/operators';
import { SeoService } from '../../services/seo.service';
import { StateService } from '../../services/state.service';
import { WebsocketService } from '../../services/websocket.service';
import { BisqApiService } from '../bisq-api.service';
import { Trade } from '../bisq.interfaces';
@Component({
selector: 'app-bisq-dashboard',
templateUrl: './bisq-dashboard.component.html',
styleUrls: ['./bisq-dashboard.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BisqDashboardComponent implements OnInit {
tickers$: Observable<any>;
volumes$: Observable<any>;
trades$: Observable<Trade[]>;
sort$ = new BehaviorSubject<string>('trades');
allowCryptoCoins = ['usdc', 'l-btc', 'bsq'];
constructor(
private websocketService: WebsocketService,
private bisqApiService: BisqApiService,
public stateService: StateService,
private seoService: SeoService,
) { }
ngOnInit(): void {
this.seoService.setTitle($localize`:@@meta.title.bisq.markets:Markets`);
this.seoService.setDescription($localize`:@@meta.description.bisq.markets:Explore the full Bitcoin ecosystem with The Mempool Open Source Project&reg;. See Bisq market prices, trading activity, and more.`);
this.websocketService.want(['blocks']);
this.volumes$ = this.bisqApiService.getAllVolumesDay$()
.pipe(
map((volumes) => {
const data = volumes.map((volume) => {
return {
time: volume.period_start,
value: volume.volume,
};
});
const linesData = volumes.map((volume) => {
return {
time: volume.period_start,
value: volume.num_trades,
};
});
return {
data: data,
linesData: linesData,
};
})
);
const getMarkets = this.bisqApiService.getMarkets$().pipe(share());
this.tickers$ = combineLatest([
this.bisqApiService.getMarketsTicker$(),
getMarkets,
this.bisqApiService.getMarketVolumesByTime$('7d'),
])
.pipe(
map(([tickers, markets, volumes]) => {
const newTickers = [];
for (const t in tickers) {
if (this.stateService.env.BASE_MODULE !== 'bisq') {
const pair = t.split('_');
if (pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1) {
continue;
}
}
const mappedTicker: any = tickers[t];
mappedTicker.pair_url = t;
mappedTicker.pair = t.replace('_', '/').toUpperCase();
mappedTicker.market = markets[t];
mappedTicker.volume = volumes[t];
mappedTicker.name = `${mappedTicker.market.rtype === 'crypto' ? mappedTicker.market.lname : mappedTicker.market.rname} (${mappedTicker.market.rtype === 'crypto' ? mappedTicker.market.lsymbol : mappedTicker.market.rsymbol}`;
newTickers.push(mappedTicker);
}
return newTickers;
}),
switchMap((tickers) => combineLatest([this.sort$, of(tickers)])),
map(([sort, tickers]) => {
if (sort === 'trades') {
tickers.sort((a, b) => (b.volume && b.volume.num_trades || 0) - (a.volume && a.volume.num_trades || 0));
} else if (sort === 'volumes') {
tickers.sort((a, b) => (b.volume && b.volume.volume || 0) - (a.volume && a.volume.volume || 0));
} else if (sort === 'name') {
tickers.sort((a, b) => a.name.localeCompare(b.name));
}
return tickers;
})
);
this.trades$ = combineLatest([
this.bisqApiService.getMarketTrades$('all'),
getMarkets,
])
.pipe(
map(([trades, markets]) => {
if (this.stateService.env.BASE_MODULE !== 'bisq') {
trades = trades.filter((trade) => {
const pair = trade.market.split('_');
return !(pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1);
});
}
return trades.map((trade => {
trade._market = markets[trade.market];
return trade;
}));
})
);
}
trackByFn(index: number) {
return index;
}
sort(by: string) {
this.sort$.next(by);
}
}

View File

@@ -1 +0,0 @@
<fa-icon [icon]="iconProp" [fixedWidth]="true" [ngStyle]="{ 'color': '#' + color }"></fa-icon>

View File

@@ -1,87 +0,0 @@
import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core';
import { IconPrefix, IconName } from '@fortawesome/fontawesome-common-types';
@Component({
selector: 'app-bisq-icon',
templateUrl: './bisq-icon.component.html',
styleUrls: ['./bisq-icon.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BisqIconComponent implements OnChanges {
@Input() txType: string;
iconProp: [IconPrefix, IconName] = ['fas', 'leaf'];
color: string;
constructor() { }
ngOnChanges() {
switch (this.txType) {
case 'UNVERIFIED':
this.iconProp[1] = 'question';
this.color = 'ffac00';
break;
case 'INVALID':
this.iconProp[1] = 'exclamation-triangle';
this.color = 'ff4500';
break;
case 'GENESIS':
this.iconProp[1] = 'rocket';
this.color = '25B135';
break;
case 'TRANSFER_BSQ':
this.iconProp[1] = 'retweet';
this.color = 'a3a3a3';
break;
case 'PAY_TRADE_FEE':
this.iconProp[1] = 'leaf';
this.color = '689f43';
break;
case 'PROPOSAL':
this.iconProp[1] = 'file-alt';
this.color = '6c8b3b';
break;
case 'COMPENSATION_REQUEST':
this.iconProp[1] = 'money-bill';
this.color = '689f43';
break;
case 'REIMBURSEMENT_REQUEST':
this.iconProp[1] = 'money-bill';
this.color = '04a908';
break;
case 'BLIND_VOTE':
this.iconProp[1] = 'eye-slash';
this.color = '07579a';
break;
case 'VOTE_REVEAL':
this.iconProp[1] = 'eye';
this.color = '4AC5FF';
break;
case 'LOCKUP':
this.iconProp[1] = 'lock';
this.color = '0056c4';
break;
case 'UNLOCK':
this.iconProp[1] = 'lock-open';
this.color = '1d965f';
break;
case 'ASSET_LISTING_FEE':
this.iconProp[1] = 'file-alt';
this.color = '6c8b3b';
break;
case 'PROOF_OF_BURN':
this.iconProp[1] = 'file-alt';
this.color = '6c8b3b';
break;
case 'IRREGULAR':
this.iconProp[1] = 'exclamation-circle';
this.color = 'ffd700';
break;
default:
this.iconProp[1] = 'question';
this.color = 'ffac00';
}
// @ts-ignore
this.iconProp = this.iconProp.slice();
}
}

View File

@@ -1,124 +0,0 @@
<div class="container-xl">
<br>
<div class="row row-cols-1 row-cols-md-2">
<div class="col mb-4">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title" i18n="bisq-dashboard.price-index-title">Bisq Price Index</h5>
<div class="big-fiat">
<span *ngIf="usdPrice$ | async as usdPrice; else loading">
<span [appColoredPrice]="usdPrice">{{ usdPrice | currency:'USD':'symbol':'1.2-2' }}</span>
</span>
</div>
</div>
</div>
</div>
<div class="col mb-4">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title" i18n="bisq-dashboard.market-price-title">Bisq Market Price</h5>
<div class="big-fiat">
<span class="green-color" *ngIf="bisqMarketPrice; else loading">
<span [appColoredPrice]="bisqMarketPrice">{{ bisqMarketPrice | currency:'USD':'symbol':'1.2-2' }}</span>
</span>
</div>
</div>
</div>
</div>
</div>
<div class="row row-cols-1 row-cols-md-2">
<div class="col mb-4">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title">US Dollar - BTC/USD</h5>
<div class="chart-container">
<ng-container *ngIf="hlocData$ | async as hlocData; else loadingSpinner">
<app-lightweight-charts [height]="300" [data]="hlocData.hloc" [volumeData]="hlocData.volume" [precision]="2"></app-lightweight-charts>
</ng-container>
</div>
</div>
</div>
</div>
<div class="col mb-4">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title" i18n="Bisq markets title">Bisq Trading Volume</h5>
<div class="chart-container">
<ng-container *ngIf="volumes$ | async as volumes; else loadingSpinner">
<app-lightweight-charts-area [height]="300" [data]="volumes.data" [lineData]="volumes.linesData"></app-lightweight-charts-area>
</ng-container>
</div>
</div>
</div>
</div>
</div>
<div class="row row-cols-1 row-cols-md-2">
<ng-container *ngIf="{ value: (tickers$ | async) } as tickers">
<div class="col mb-4">
<div class="card">
<div class="card-body">
<h5 class="card-title text-center">
<ng-template [ngIf]="stateService.env.BASE_MODULE === 'bisq'" [ngIfElse]="nonOfficialMarkets" i18n="Bisq All Markets">Markets</ng-template>
<ng-template #nonOfficialMarkets i18n="Bisq Bitcoin Markets">Bitcoin Markets</ng-template>
</h5>
<div class="table-container">
<table class="table table-borderless table-striped">
<thead>
<th><ng-container i18n>Currency</ng-container> <button [disabled]="(sort$ | async) === 'name'" class="btn btn-link btn-sm" (click)="sort('name')"><fa-icon [icon]="['fas', 'chevron-down']" [fixedWidth]="true"></fa-icon></button></th>
<th i18n>Price</th>
<th><ng-container i18n="Trades amount 7D">Trades (7d)</ng-container> <button [disabled]="(sort$ | async) === 'trades'" class="btn btn-link btn-sm" (click)="sort('trades')"><fa-icon [icon]="['fas', 'chevron-down']" [fixedWidth]="true"></fa-icon></button></th>
</thead>
<tbody *ngIf="tickers.value; else loadingTmpl">
<tr *ngFor="let ticker of tickers.value; trackBy: trackByFn;">
<td><a [routerLink]="['/market' | relativeUrl, ticker.pair_url]">{{ ticker.name }})</a></td>
<td>
<app-fiat *ngIf="ticker.market.rtype === 'crypto'; else fiat" [value]="ticker.last * 100000000"></app-fiat>
<ng-template #fiat>
<span class="green-color">{{ ticker.last | currency: ticker.market.rsymbol }}</span>
</ng-template>
</td>
<td>{{ ticker.volume?.num_trades ? ticker.volume?.num_trades : 0 }}</td>
</tr>
</tbody>
</table>
</div>
<div class="text-center"><a href="" [routerLink]="['/markets' | relativeUrl]" i18n="dashboard.view-more">View more &raquo;</a></div>
</div>
</div>
</div>
<div class="col mb-4">
<div class="card">
<div class="card-body">
<h5 class="card-title text-center" i18n="Latest Trades header">Latest Trades</h5>
<app-bisq-trades [trades$]="trades$" view="small"></app-bisq-trades>
<div class="text-center"><a href="" [routerLink]="['/markets' | relativeUrl]" i18n="dashboard.view-more">View more &raquo;</a></div>
</div>
</div>
</div>
</ng-container>
</div>
</div>
<ng-template #loadingTmpl>
<tr *ngFor="let i of [1,2,3,4,5,6,7,8,9,10]">
<td *ngFor="let j of [1, 2, 3]"><span class="skeleton-loader"></span></td>
</tr>
</ng-template>
<ng-template #loadingSpinner>
<div class="text-center loadingGraphs">
<div class="spinner-border text-light"></div>
</div>
</ng-template>
<ng-template #loading>
<div class="skeleton-loader shorter"></div>
</ng-template>

View File

@@ -1,112 +0,0 @@
#volumeHolder {
height: 500px;
background-color: #000;
overflow: hidden;
display: flex;
justify-content: center;
}
.table {
max-width: 100%;
overflow: scroll;
}
.loadingGraphs {
position: relative;
top: 45%;
z-index: 100;
}
.table-container {
overflow: scroll;
scrollbar-width: none;
font-size: 13px;
&::-webkit-scrollbar {
display: none;
}
@media(min-width: 576px){
font-size: 16px;
}
thead th{
text-align: right;
&:first-child {
text-align: left;
}
&:nth-child(3) {
display: none;
@media(min-width: 1100px){
display: table-cell;
}
}
}
tr {
td {
text-align: right;
&:first-child {
text-align: left;
}
&:nth-child(3) {
display: none;
@media(min-width: 1100px){
display: table-cell;
}
}
}
}
}
.chart-container {
height: 300px;
}
.big-fiat {
color: #3bcc49;
font-size: 26px;
}
.card {
background-color: #1d1f31;
height: 100%;
}
.card-title {
color: #4a68b9;
font-size: 1rem;
}
.info-block {
float: left;
width: 350px;
line-height: 25px;
}
.progress {
display: inline-flex;
width: 100%;
background-color: #2d3348;
height: 1.1rem;
}
.bg-warning {
background-color: #b58800 !important;
}
.skeleton-loader {
max-width: 100%;
&.shorter {
max-width: 150px;
}
}
.more-padding {
padding: 1.25rem 2rem 1.25rem 2rem;
}
.graph-card {
height: 100%;
@media (min-width: 992px) {
height: 385px;
}
}

View File

@@ -1,193 +0,0 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { Observable, combineLatest, BehaviorSubject, of } from 'rxjs';
import { map, share, switchMap } from 'rxjs/operators';
import { SeoService } from '../../services/seo.service';
import { StateService } from '../../services/state.service';
import { WebsocketService } from '../../services/websocket.service';
import { BisqApiService } from '../bisq-api.service';
import { Trade } from '../bisq.interfaces';
@Component({
selector: 'app-main-bisq-dashboard',
templateUrl: './bisq-main-dashboard.component.html',
styleUrls: ['./bisq-main-dashboard.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BisqMainDashboardComponent implements OnInit {
tickers$: Observable<any>;
volumes$: Observable<any>;
trades$: Observable<Trade[]>;
sort$ = new BehaviorSubject<string>('trades');
hlocData$: Observable<any>;
usdPrice$: Observable<number>;
isLoadingGraph = true;
bisqMarketPrice = 0;
allowCryptoCoins = ['usdc', 'l-btc', 'bsq'];
constructor(
private websocketService: WebsocketService,
private bisqApiService: BisqApiService,
public stateService: StateService,
private seoService: SeoService,
) { }
ngOnInit(): void {
this.seoService.resetTitle();
this.seoService.resetDescription();
this.websocketService.want(['blocks']);
this.usdPrice$ = this.stateService.conversions$.asObservable().pipe(
map((conversions) => conversions.USD)
);
this.volumes$ = this.bisqApiService.getAllVolumesDay$()
.pipe(
map((volumes) => {
const data = volumes.map((volume) => {
return {
time: volume.period_start,
value: volume.volume,
};
});
const linesData = volumes.map((volume) => {
return {
time: volume.period_start,
value: volume.num_trades,
};
});
return {
data: data,
linesData: linesData,
};
})
);
const getMarkets = this.bisqApiService.getMarkets$().pipe(share());
this.tickers$ = combineLatest([
this.bisqApiService.getMarketsTicker$(),
getMarkets,
this.bisqApiService.getMarketVolumesByTime$('7d'),
])
.pipe(
map(([tickers, markets, volumes]) => {
const newTickers = [];
for (const t in tickers) {
if (this.stateService.env.BASE_MODULE !== 'bisq') {
const pair = t.split('_');
if (pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1) {
continue;
}
}
const mappedTicker: any = tickers[t];
mappedTicker.pair_url = t;
mappedTicker.pair = t.replace('_', '/').toUpperCase();
mappedTicker.market = markets[t];
mappedTicker.volume = volumes[t];
mappedTicker.name = `${mappedTicker.market.rtype === 'crypto' ? mappedTicker.market.lname : mappedTicker.market.rname} (${mappedTicker.market.rtype === 'crypto' ? mappedTicker.market.lsymbol : mappedTicker.market.rsymbol}`;
newTickers.push(mappedTicker);
}
return newTickers;
}),
switchMap((tickers) => combineLatest([this.sort$, of(tickers)])),
map(([sort, tickers]) => {
if (sort === 'trades') {
tickers.sort((a, b) => (b.volume && b.volume.num_trades || 0) - (a.volume && a.volume.num_trades || 0));
} else if (sort === 'volumes') {
tickers.sort((a, b) => (b.volume && b.volume.volume || 0) - (a.volume && a.volume.volume || 0));
} else if (sort === 'name') {
tickers.sort((a, b) => a.name.localeCompare(b.name));
}
return tickers.slice(0, 10);
})
);
this.trades$ = combineLatest([
this.bisqApiService.getMarketTrades$('all'),
getMarkets,
])
.pipe(
map(([trades, markets]) => {
if (this.stateService.env.BASE_MODULE !== 'bisq') {
trades = trades.filter((trade) => {
const pair = trade.market.split('_');
return !(pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1);
});
}
return trades.map((trade => {
trade._market = markets[trade.market];
return trade;
})).slice(0, 10);
})
);
this.hlocData$ = this.bisqApiService.getMarketsHloc$('btc_usd', 'day')
.pipe(
map((hlocData) => {
this.isLoadingGraph = false;
hlocData = hlocData.map((h) => {
h.time = h.period_start;
return h;
});
const hlocVolume = hlocData.map((h) => {
return {
time: h.time,
value: h.volume_right,
color: h.close > h.avg ? 'rgba(0, 41, 74, 0.7)' : 'rgba(0, 41, 74, 1)',
};
});
// Add whitespace
if (hlocData.length > 1) {
const newHloc = [];
newHloc.push(hlocData[0]);
const period = 86400;
let periods = 0;
const startingDate = hlocData[0].period_start;
let index = 1;
while (true) {
periods++;
if (hlocData[index].period_start > startingDate + period * periods) {
newHloc.push({
time: startingDate + period * periods,
});
} else {
newHloc.push(hlocData[index]);
index++;
if (!hlocData[index]) {
break;
}
}
}
hlocData = newHloc;
}
this.bisqMarketPrice = hlocData[hlocData.length - 1].close;
return {
hloc: hlocData,
volume: hlocVolume,
};
}),
);
}
trackByFn(index: number) {
return index;
}
sort(by: string) {
this.sort$.next(by);
}
}

View File

@@ -1,112 +0,0 @@
<div class="container-xl">
<ng-container *ngIf="hlocData$ | async as hlocData; else loadingSpinner">
<ng-container *ngIf="currency$ | async as currency; else loadingSpinner">
<h1>{{ currency.market.rtype === 'crypto' ? currency.market.lname : currency.market.rname }} - {{ currency.pair }}</h1>
<div class="priceheader">
<ng-container *ngIf="currency.market.rtype === 'fiat'; else headerPriceCrypto"><span class="green-color">{{ hlocData.hloc[hlocData.hloc.length - 1].close | currency: currency.market.rsymbol }}</span></ng-container>
<ng-template #headerPriceCrypto>{{ hlocData.hloc[hlocData.hloc.length - 1].close | number: '1.' + currency.market.rprecision + '-' + currency.market.rprecision }} {{ currency.market.rsymbol }}</ng-template>
</div>
<form [formGroup]="radioGroupForm" class="mb-3 radio-form">
<div class="btn-group btn-group-toggle" name="radioBasic">
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'half_hour'">
<input type="radio" [value]="'half_hour'" (click)="setFragment('half_hour')" formControlName="interval"> 30M
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'hour'">
<input type="radio" [value]="'hour'" (click)="setFragment('hour')" formControlName="interval"> 1H
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'half_day'">
<input type="radio" [value]="'half_day'" (click)="setFragment('half_day')" formControlName="interval"> 12H
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'day'">
<input type="radio" [value]="'day'" (click)="setFragment('day')" formControlName="interval"> 1D
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'week'">
<input type="radio" [value]="'week'" (click)="setFragment('week')" formControlName="interval"> 1W
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'month'">
<input type="radio" [value]="'month'" (click)="setFragment('month')" formControlName="interval"> 1M
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'year'">
<input type="radio" [value]="'year'" (click)="setFragment('year')" formControlName="interval"> 1Y
</label>
</div>
</form>
<div class="clearfix"></div>
<div id="graphHolder">
<div class="text-center loadingChart" [hidden]="!isLoadingGraph">
<div class="spinner-border text-light"></div>
</div>
<app-lightweight-charts [data]="hlocData.hloc" [volumeData]="hlocData.volume" [precision]="currency.market.rtype === 'crypto' ? currency.market.lprecision : currency.market.rprecision"></app-lightweight-charts>
</div>
<br>
<ng-container *ngIf="offers$ | async as offers; else loadingSpinner">
<div class="row row-cols-1 row-cols-md-2">
<ng-container *ngTemplateOutlet="offersList; context: { offers: offers.buys, direction: 'BUY', market: currency.market }"></ng-container>
<ng-container *ngTemplateOutlet="offersList; context: { offers: offers.sells, direction: 'SELL', market: currency.market }"></ng-container>
</div>
</ng-container>
<br><br>
<ng-container *ngIf="trades$ | async as trades; else loadingSpinner">
<h2 i18n="Latest Trades header">Latest Trades</h2>
<app-bisq-trades [trades$]="trades$" [market]="currency.market"></app-bisq-trades>
</ng-container>
</ng-container>
</ng-container>
</div>
<ng-template #offersList let-offers="offers" let-direction="direction", let-market="market">
<div class="col">
<h2>
<ng-template [ngIf]="direction === 'BUY'" [ngIfElse]="sellOffers" i18n="Bisq Buy Offers">Buy Offers</ng-template>
<ng-template #sellOffers i18n="Bisq Sell Offers">Sell Offers</ng-template>
</h2>
<div class="table-container">
<table class="table table-borderless table-striped">
<thead>
<th i18n>Price</th>
<th><ng-container *ngTemplateOutlet="amount; context: {$implicit: market.lsymbol }"></ng-container></th>
<th><ng-container *ngTemplateOutlet="amount; context: {$implicit: market.rsymbol }"></ng-container></th>
</thead>
<tbody>
<tr *ngFor="let offer of offers">
<td>
<ng-container *ngIf="market.rtype === 'fiat'; else priceCrypto"><span class="green-color">{{ offer.price | currency: market.rsymbol }}</span></ng-container>
<ng-template #priceCrypto>{{ offer.price | number: '1.2-' + market.rprecision }} <span class="symbol">{{ market.rsymbol }}</span></ng-template>
</td>
<td>
<ng-container *ngIf="market.ltype === 'fiat'; else amountCrypto"><span class="green-color">{{ offer.amount | currency: market.rsymbol }}</span></ng-container>
<ng-template #amountCrypto>{{ offer.amount | number: '1.2-' + market.lprecision }} <span class="symbol">{{ market.lsymbol }}</span></ng-template>
</td>
<td>
<ng-container *ngIf="market.rtype === 'fiat'; else volumeCrypto"><span class="green-color">{{ offer.volume | currency: market.rsymbol }}</span></ng-container>
<ng-template #volumeCrypto>{{ offer.volume | number: '1.2-' + market.rprecision }} <span class="symbol">{{ market.rsymbol }}</span></ng-template>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</ng-template>
<ng-template #loadingSpinner>
<br>
<br>
<div class="text-center">
<div class="spinner-border text-light"></div>
</div>
</ng-template>
<ng-template #amount let-i i18n="Trade amount (Symbol)">Amount ({{ i }})</ng-template>

View File

@@ -1,46 +0,0 @@
.priceheader {
font-size: 24px;
@media(min-width: 576px){
float: left;
}
}
.radio-form {
@media(min-width: 576px){
float: right;
}
}
.loadingChart {
z-index: 100;
position: absolute;
top: 50%;
left: 50%;
}
#graphHolder {
height: 550px;
overflow: hidden;
}
.col {
&:last-child{
margin-top: 50px;
@media(min-width: 576px){
margin-top: 0px;
}
}
}
.table-container {
overflow: scroll;
-ms-overflow-style: none;
scrollbar-width: none;
font-size: 13px;
@media(min-width: 576px){
font-size: 16px;
}
&::-webkit-scrollbar {
display: none;
}
}

View File

@@ -1,159 +0,0 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { combineLatest, merge, Observable, of } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { SeoService } from '../../services/seo.service';
import { WebsocketService } from '../../services/websocket.service';
import { BisqApiService } from '../bisq-api.service';
import { OffersMarket, Trade } from '../bisq.interfaces';
@Component({
selector: 'app-bisq-market',
templateUrl: './bisq-market.component.html',
styleUrls: ['./bisq-market.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BisqMarketComponent implements OnInit, OnDestroy {
hlocData$: Observable<any>;
currency$: Observable<any>;
offers$: Observable<OffersMarket>;
trades$: Observable<Trade[]>;
radioGroupForm: UntypedFormGroup;
defaultInterval = 'day';
isLoadingGraph = false;
constructor(
private websocketService: WebsocketService,
private route: ActivatedRoute,
private bisqApiService: BisqApiService,
private formBuilder: UntypedFormBuilder,
private seoService: SeoService,
private router: Router,
) { }
ngOnInit(): void {
this.radioGroupForm = this.formBuilder.group({
interval: [this.defaultInterval],
});
if (['half_hour', 'hour', 'half_day', 'day', 'week', 'month', 'year', 'auto'].indexOf(this.route.snapshot.fragment) > -1) {
this.radioGroupForm.controls.interval.setValue(this.route.snapshot.fragment, { emitEvent: false });
}
this.currency$ = this.bisqApiService.getMarkets$()
.pipe(
switchMap((markets) => combineLatest([of(markets), this.route.paramMap])),
map(([markets, routeParams]) => {
const pair = routeParams.get('pair');
const pairUpperCase = pair.replace('_', '/').toUpperCase();
this.seoService.setTitle($localize`:@@meta.title.bisq.market:Bisq market: ${pairUpperCase}`);
this.seoService.setDescription($localize`:@@meta.description.bisq.market:See price history, current buy/sell offers, and latest trades for the ${pairUpperCase} market on Bisq.`);
return {
pair: pairUpperCase,
market: markets[pair],
};
})
);
this.trades$ = this.route.paramMap
.pipe(
map(routeParams => routeParams.get('pair')),
switchMap((marketPair) => this.bisqApiService.getMarketTrades$(marketPair)),
);
this.offers$ = this.route.paramMap
.pipe(
map(routeParams => routeParams.get('pair')),
switchMap((marketPair) => this.bisqApiService.getMarketOffers$(marketPair)),
map((offers) => offers[Object.keys(offers)[0]])
);
this.hlocData$ = combineLatest([
this.route.paramMap,
merge(this.radioGroupForm.get('interval').valueChanges, of(this.radioGroupForm.get('interval').value)),
])
.pipe(
switchMap(([routeParams, interval]) => {
this.isLoadingGraph = true;
const pair = routeParams.get('pair');
return this.bisqApiService.getMarketsHloc$(pair, interval);
}),
map((hlocData) => {
this.isLoadingGraph = false;
hlocData = hlocData.map((h) => {
h.time = h.period_start;
return h;
});
const hlocVolume = hlocData.map((h) => {
return {
time: h.time,
value: h.volume_right,
color: h.close > h.avg ? 'rgba(0, 41, 74, 0.7)' : 'rgba(0, 41, 74, 1)',
};
});
// Add whitespace
if (hlocData.length > 1) {
const newHloc = [];
newHloc.push(hlocData[0]);
const period = this.getUnixTimestampFromInterval(this.radioGroupForm.get('interval').value); // temp
let periods = 0;
const startingDate = hlocData[0].period_start;
let index = 1;
while (true) {
periods++;
if (hlocData[index].period_start > startingDate + period * periods) {
newHloc.push({
time: startingDate + period * periods,
});
} else {
newHloc.push(hlocData[index]);
index++;
if (!hlocData[index]) {
break;
}
}
}
hlocData = newHloc;
}
return {
hloc: hlocData,
volume: hlocVolume,
};
}),
);
}
setFragment(fragment: string) {
this.router.navigate([], {
relativeTo: this.route,
queryParamsHandling: 'merge',
fragment: fragment
});
}
ngOnDestroy(): void {
this.websocketService.stopTrackingBisqMarket();
}
getUnixTimestampFromInterval(interval: string): number {
switch (interval) {
case 'minute': return 60;
case 'half_hour': return 1800;
case 'hour': return 3600;
case 'half_day': return 43200;
case 'day': return 86400;
case 'week': return 604800;
case 'month': return 2592000;
case 'year': return 31579200;
}
}
}

View File

@@ -1,86 +0,0 @@
<div class="container-xl">
<h1 style="float: left;" i18n="BSQ statistics header">BSQ statistics</h1>
<br>
<div class="clearfix"></div>
<div class="row">
<div class="col-md">
<table class="table table-borderless table-striped">
<tbody *ngIf="!isLoading; else loadingTemplate">
<tr>
<td class="td-width" i18n="BSQ existing amount">Existing amount</td>
<td>{{ (stats.minted - stats.burnt) | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
</tr>
<tr>
<td i18n="BSQ minted amount">Minted amount</td>
<td>{{ stats.minted | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
</tr>
<tr>
<td i18n="BSQ burnt amount">Burnt amount</td>
<td>{{ stats.burnt | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
</tr>
<tr>
<td i18n="BSQ addresses">Addresses</td>
<td>{{ stats.addresses | number }}</td>
</tr>
<tr>
<td i18n="BSQ unspent transaction outputs">Unspent TXOs</td>
<td>{{ stats.unspent_txos | number }}</td>
</tr>
<tr>
<td i18n="BSQ spent transaction outputs">Spent TXOs</td>
<td>{{ stats.spent_txos | number }}</td>
</tr>
<tr>
<td i18n>Price</td>
<td><app-fiat [value]="price"></app-fiat></td>
</tr>
<tr>
<td i18n="BSQ token market cap">Market cap</td>
<td><app-fiat [value]="price * (stats.minted - stats.burnt)"></app-fiat></td>
</tr>
</tbody>
</table>
</div>
<div class="col-md"></div>
</div>
</div>
<ng-template #loadingTemplate>
<tbody>
<tr>
<td class="td-width" i18n>Existing amount</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td i18n>Minted amount</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td i18n>Burnt amount</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td i18n>Addresses</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td i18n>Unspent TXOs</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td>Spent TXOs</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td i18n>Price</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td i18n>Market cap</td>
<td><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</ng-template>

View File

@@ -1,18 +0,0 @@
.td-width {
width: 250px;
}
@media (max-width: 767.98px) {
.td-width {
width: 175px;
}
}
.fiat {
display: block;
font-size: 13px;
@media (min-width: 768px) {
font-size: 14px;
display: inline-block;
}
}

View File

@@ -1,42 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { BisqApiService } from '../bisq-api.service';
import { BisqStats } from '../bisq.interfaces';
import { SeoService } from '../../services/seo.service';
import { StateService } from '../../services/state.service';
import { WebsocketService } from '../../services/websocket.service';
@Component({
selector: 'app-bisq-stats',
templateUrl: './bisq-stats.component.html',
styleUrls: ['./bisq-stats.component.scss']
})
export class BisqStatsComponent implements OnInit {
isLoading = true;
stats: BisqStats;
price: number;
constructor(
private websocketService: WebsocketService,
private bisqApiService: BisqApiService,
private seoService: SeoService,
private stateService: StateService,
) { }
ngOnInit() {
this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@2a30a4cdb123a03facc5ab8c5b3e6d8b8dbbc3d4:BSQ statistics`);
this.seoService.setDescription($localize`:@@meta.description.bisq.stats:See high-level stats on the BSQ economy: supply metrics, number of addresses, BSQ price, market cap, and more.`);
this.stateService.bsqPrice$
.subscribe((bsqPrice) => {
this.price = bsqPrice;
});
this.bisqApiService.getStats$()
.subscribe((stats) => {
this.isLoading = false;
this.stats = stats;
});
}
}

View File

@@ -1,46 +0,0 @@
<div class="table-container">
<table class="table table-borderless table-striped">
<thead>
<th i18n>Date</th>
<th *ngIf="view === 'all'" i18n>Price</th>
<th><ng-container *ngTemplateOutlet="amount; context: {$implicit: 'BTC' }"></ng-container></th>
<th>
<ng-template [ngIf]="market" [ngIfElse]="noMarket"><ng-container *ngTemplateOutlet="amount; context: {$implicit: market.lsymbol === 'BTC' ? market.rsymbol : market.lsymbol }"></ng-container></ng-template>
<ng-template #noMarket i18n>Amount</ng-template>
</th>
</thead>
<tbody *ngIf="(trades$ | async) as trades; else loadingTmpl">
<tr *ngFor="let trade of trades;">
<td>
&lrm;{{ trade.trade_date | date:'yyyy-MM-dd HH:mm' }}
</td>
<td *ngIf="view === 'all'">
<ng-container *ngIf="(trade._market || market).rtype === 'fiat'; else priceCrypto"><span class="green-color">{{ trade.price | currency: (trade._market || market).rsymbol }}</span></ng-container>
<ng-template #priceCrypto>{{ trade.price | number: '1.2-' + (trade._market || market).rprecision }} <span class="symbol">{{ (trade._market || market).rsymbol }}</span></ng-template>
</td>
<ng-container *ngTemplateOutlet="(trade._market || market).rsymbol === 'BTC' ? tradeVolume : tradeAmount"></ng-container>
<ng-container *ngTemplateOutlet="(trade._market || market).rsymbol === 'BTC' ? tradeAmount : tradeVolume"></ng-container>
<ng-template #tradeAmount>
<td>
<ng-container *ngIf="(trade._market || market).ltype === 'fiat'; else amountCrypto"><span class="green-color">{{ trade.amount | currency: (trade._market || market).rsymbol }}</span></ng-container>
<ng-template #amountCrypto>{{ trade.amount | number: '1.2-' + (trade._market || market).lprecision }} <span class="symbol">{{ (trade._market || market).lsymbol }}</span></ng-template>
</td>
</ng-template>
<ng-template #tradeVolume>
<td>
<ng-container *ngIf="(trade._market || market).rtype === 'fiat'; else volumeCrypto"><span class="green-color">{{ trade.volume | currency: (trade._market || market).rsymbol }}</span></ng-container>
<ng-template #volumeCrypto>{{ trade.volume | number: '1.2-' + (trade._market || market).rprecision }} <span class="symbol">{{ (trade._market || market).rsymbol }}</span></ng-template>
</td>
</ng-template>
</tr>
</tbody>
</table>
</div>
<ng-template #loadingTmpl>
<tr *ngFor="let i of [1,2,3,4,5,6,7,8,9,10]">
<td *ngFor="let j of loadingColumns"><span class="skeleton-loader"></span></td>
</tr>
</ng-template>
<ng-template #amount let-i i18n="Trade amount (Symbol)">Amount ({{ i }})</ng-template>

View File

@@ -1,38 +0,0 @@
.table-container {
overflow: scroll;
scrollbar-width: none;
font-size: 13px;
&::-webkit-scrollbar {
display: none;
}
@media(min-width: 576px){
font-size: 16px;
}
thead th{
text-align: right;
&:first-child{
text-align: left;
}
&:nth-child(2) {
display: none;
@media(min-width: 1100px){
display: table-cell;
}
}
}
tr {
td {
text-align: right;
&:first-child{
text-align: left;
}
&:nth-child(2) {
display: none;
@media(min-width: 1100px){
display: table-cell;
}
}
}
}
}

View File

@@ -1,22 +0,0 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
@Component({
selector: 'app-bisq-trades',
templateUrl: './bisq-trades.component.html',
styleUrls: ['./bisq-trades.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BisqTradesComponent implements OnChanges {
@Input() trades$: Observable<any>;
@Input() market: any;
@Input() view: 'all' | 'small' = 'all';
loadingColumns = [1, 2, 3, 4];
ngOnChanges() {
if (this.view === 'small') {
this.loadingColumns = [1, 2, 3];
}
}
}

View File

@@ -1,36 +0,0 @@
<div class="box">
<div class="row">
<div class="col-md">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width" i18n="transaction.inputs">Inputs</td>
<td>{{ totalInput / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
</tr>
<tr>
<td i18n="transaction.outputs">Outputs</td>
<td>{{ totalOutput / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
</tr>
<tr>
<td i18n="asset.issued-amount|Liquid Asset issued amount">Issued amount</td>
<td>{{ totalIssued / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
</tr>
</tbody>
</table>
</div>
<div class="col-md">
<table class="table table-borderless table-striped">
<tbody class="mobile-even">
<tr>
<td class="td-width" i18n>Type</td>
<td><app-bisq-icon class="mr-1" [txType]="tx.txType"></app-bisq-icon> {{ tx.txTypeDisplayString }}</td>
</tr>
<tr>
<td i18n="transaction.version">Version</td>
<td>{{ tx.txVersion }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

View File

@@ -1,22 +0,0 @@
@media (max-width: 767.98px) {
.td-width {
width: 150px;
}
.mobile-even tr:nth-of-type(even) {
background-color: #181b2d;
}
.mobile-even tr:nth-of-type(odd) {
background-color: inherit;
}
}
.table {
tr td {
&:last-child{
text-align: right;
@media(min-width: 768px){
text-align: left;
}
}
}
}

View File

@@ -1,26 +0,0 @@
import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core';
import { BisqTransaction } from '../../bisq/bisq.interfaces';
@Component({
selector: 'app-bisq-transaction-details',
templateUrl: './bisq-transaction-details.component.html',
styleUrls: ['./bisq-transaction-details.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BisqTransactionDetailsComponent implements OnChanges {
@Input() tx: BisqTransaction;
totalInput: number;
totalOutput: number;
totalIssued: number;
constructor() { }
ngOnChanges() {
this.totalInput = this.tx.inputs.filter((input) => input.isVerified).reduce((acc, input) => acc + input.bsqAmount, 0);
this.totalOutput = this.tx.outputs.filter((output) => output.isVerified).reduce((acc, output) => acc + output.bsqAmount, 0);
this.totalIssued = this.tx.outputs
.filter((output) => output.isVerified && output.txOutputType === 'ISSUANCE_CANDIDATE_OUTPUT')
.reduce((acc, output) => acc + output.bsqAmount, 0);
}
}

View File

@@ -1,216 +0,0 @@
<div class="container-xl">
<ng-template [ngIf]="!isLoading && !error">
<div class="title-block">
<div class="title">
<h1 i18n="shared.transaction">Transaction</h1>
</div>
<span class="tx-link">
<span class="txid">
<app-truncate [text]="bisqTx.id" [lastChars]="12" [link]="['/tx/' | relativeUrl, bisqTx.id]">
<app-clipboard [text]="bisqTx.id"></app-clipboard>
</app-truncate>
</span>
</span>
<span class="grow"></span>
<div class="container-buttons">
<div *ngIf="(latestBlock$ | async) as latestBlock">
<app-confirmations [chainTip]="latestBlock?.height" [height]="bisqTx.blockHeight" [hideUnconfirmed]="true" buttonClass="float-right"></app-confirmations>
</div>
</div>
</div>
<div class="clearfix"></div>
<div class="box transaction-container">
<div class="row">
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td i18n="block.timestamp">Timestamp</td>
<td>
&lrm;{{ bisqTx.time | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline">
<i class="symbol">(<app-time kind="since" [time]="bisqTx.time / 1000" [fastRender]="true"></app-time>)</i>
</div>
</td>
</tr>
<tr>
<td class="td-width" i18n="transaction.included-in-block|Transaction included in block">Included in block</td>
<td>
<a [routerLink]="['/block/' | relativeUrl, bisqTx.blockHash]" [state]="{ data: { blockHeight: bisqTx.blockHeight } }">{{ bisqTx.blockHeight }}</a>
</td>
</tr>
<tr>
<td class="td-width" i18n="transaction.features|Transaction features">Features</td>
<td>
<app-tx-features *ngIf="tx; else loadingTx" [tx]="tx"></app-tx-features>
<ng-template #loadingTx>
<span class="skeleton-loader"></span>
</ng-template>
</td>
</tr>
</tbody>
</table>
</div>
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width" i18n="BSQ burnt amount">Burnt amount</td>
<td>
{{ bisqTx.burntFee / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span> <span class="fiat"><app-bsq-amount [bsq]="bisqTx.burntFee" [forceFiat]="true" [green]="true"></app-bsq-amount></span>
</tr>
<tr>
<td *only-vsize i18n="transaction.fee-per-vbyte|Transaction fee">Fee per vByte</td>
<td *only-weight i18n="transaction.fee-per-wu|Transaction fee">Fee per weight unit</td>
<td *ngIf="!isLoadingTx; else loadingTxFee">
<app-fee-rate [fee]="tx.fee" [weight]="tx.weight"></app-fee-rate>
&nbsp;
<app-tx-fee-rating [tx]="tx"></app-tx-fee-rating>
</td>
<ng-template #loadingTxFee>
<td><span class="skeleton-loader"></span></td>
</ng-template>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<br>
<div class="title">
<h2 i18n="transaction.details">Details</h2>
</div>
<app-bisq-transaction-details [tx]="bisqTx"></app-bisq-transaction-details>
<br>
<div class="title">
<h2 i18n="transaction.inputs-and-outputs|Transaction inputs and outputs">Inputs & Outputs</h2>
</div>
<app-bisq-transfers [tx]="bisqTx"></app-bisq-transfers>
<br>
</ng-template>
<ng-template [ngIf]="isLoading && !error">
<div class="clearfix"></div>
<div class="title-block">
<div class="title">
<h1 i18n="shared.transaction">Transaction</h1>
</div>
</div>
<div class="box">
<div class="row">
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width"><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td class="td-width"><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td class="td-width"><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</div>
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width"><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td class="td-width"><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<br>
<div class="title">
<h2 i18n="transaction.details">Details</h2>
</div>
<div class="box">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</div>
<br>
<div class="title">
<h2 i18n="transaction.inputs-and-outputs|Transaction inputs and outputs">Inputs & Outputs</h2>
</div>
<div class="box">
<div class="row">
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</div>
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</ng-template>
<ng-template [ngIf]="error">
<div class="clearfix"></div>
<div class="text-center">
Error loading Bisq transaction
<br><br>
<i>{{ error.status }}: {{ error.statusText }}</i>
</div>
</ng-template>
</div>

View File

@@ -1,130 +0,0 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { BisqTransaction } from '../../bisq/bisq.interfaces';
import { switchMap, map, catchError } from 'rxjs/operators';
import { of, Observable, Subscription } from 'rxjs';
import { StateService } from '../../services/state.service';
import { Block, Transaction } from '../../interfaces/electrs.interface';
import { BisqApiService } from '../bisq-api.service';
import { SeoService } from '../../services/seo.service';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { HttpErrorResponse } from '@angular/common/http';
import { WebsocketService } from '../../services/websocket.service';
@Component({
selector: 'app-bisq-transaction',
templateUrl: './bisq-transaction.component.html',
styleUrls: ['./../../components/transaction/transaction.component.scss']
})
export class BisqTransactionComponent implements OnInit, OnDestroy {
bisqTx: BisqTransaction;
tx: Transaction;
latestBlock$: Observable<Block>;
txId: string;
price: number;
isLoading = true;
isLoadingTx = true;
error = null;
subscription: Subscription;
constructor(
private websocketService: WebsocketService,
private route: ActivatedRoute,
private bisqApiService: BisqApiService,
private electrsApiService: ElectrsApiService,
private stateService: StateService,
private seoService: SeoService,
private router: Router,
) { }
ngOnInit(): void {
this.websocketService.want(['blocks']);
this.subscription = this.route.paramMap.pipe(
switchMap((params: ParamMap) => {
this.isLoading = true;
this.isLoadingTx = true;
this.error = null;
document.body.scrollTo(0, 0);
this.txId = params.get('id') || '';
this.seoService.setTitle($localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`);
this.seoService.setDescription($localize`:@@meta.description.bisq.transaction:See inputs, outputs, transaction type, burnt amount, and more for transaction with txid ${this.txId}:INTERPOLATION:.`);
if (history.state.data) {
return of(history.state.data);
}
return this.bisqApiService.getTransaction$(this.txId)
.pipe(
catchError((bisqTxError: HttpErrorResponse) => {
if (bisqTxError.status === 404) {
return this.electrsApiService.getTransaction$(this.txId)
.pipe(
map((tx) => {
if (tx.status.confirmed) {
this.error = {
status: 200,
statusText: 'Transaction is confirmed but not available in the Bisq database, please try reloading this page.'
};
return null;
}
return tx;
}),
catchError((txError: HttpErrorResponse) => {
console.log(txError);
this.error = txError;
this.seoService.logSoft404();
return of(null);
})
);
}
this.error = bisqTxError;
this.seoService.logSoft404();
return of(null);
})
);
}),
switchMap((tx) => {
if (!tx) {
return of(null);
}
if (tx.version) {
if (this.stateService.env.BASE_MODULE === 'bisq') {
window.location.replace('https://mempool.space/tx/' + this.txId);
} else {
this.router.navigate(['/tx/', this.txId], { state: { data: tx, bsqTx: true }});
}
return of(null);
}
this.bisqTx = tx;
this.isLoading = false;
return this.electrsApiService.getTransaction$(this.txId);
}),
)
.subscribe((tx) => {
this.isLoadingTx = false;
if (!tx) {
this.seoService.logSoft404();
return;
}
this.tx = tx;
},
(error) => {
this.error = error;
});
this.latestBlock$ = this.stateService.blocks$.pipe(map((blocks) => blocks[0]));
this.stateService.bsqPrice$
.subscribe((bsqPrice) => {
this.price = bsqPrice;
});
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}

View File

@@ -1,56 +0,0 @@
<div class="container-xl" (window:resize)="onResize($event)">
<h1 style="float: left;" i18n>BSQ Transactions</h1>
<div class="d-block float-right" id="filter">
<form [formGroup]="radioGroupForm">
<ngx-bootstrap-multiselect [options]="txTypeOptions" [settings]="txTypeDropdownSettings" [texts]="txTypeDropdownTexts" formControlName="txTypes"></ngx-bootstrap-multiselect>
</form>
</div>
<br>
<div class="clearfix"></div>
<ng-container *ngIf="{ value: (transactions$ | async) } as transactions">
<table class="table table-borderless table-striped">
<thead>
<th style="width: 20%;" i18n>TXID</th>
<th class="d-none d-md-block" style="width: 100%;" i18n>Type</th>
<th style="width: 20%;" i18n>Amount</th>
<th style="width: 20%;" i18n="transaction.confirmed|Transaction Confirmed state">Confirmed</th>
<th class="d-none d-md-block" i18n>Height</th>
</thead>
<tbody *ngIf="transactions.value; else loadingTmpl">
<tr *ngFor="let tx of transactions.value[0]; trackBy: trackByFn">
<td><a [routerLink]="['/tx/' | relativeUrl, tx.id]" [state]="{ data: tx }">{{ tx.id | slice : 0 : 8 }}</a></td>
<td class="d-none d-md-block">
<app-bisq-icon class="mr-1" [txType]="tx.txType"></app-bisq-icon>
<span class="d-none d-md-inline"> {{ getStringByTxType(tx.txType) }}</span>
</td>
<td>
<app-bisq-icon class="d-inline d-md-none mr-1" [txType]="tx.txType"></app-bisq-icon>
<ng-template [ngIf]="tx.txType === 'PAY_TRADE_FEE' || tx.txType === 'ASSET_LISTING_FEE'" [ngIfElse]="defaultTxType">
{{ tx.burntFee / 100 | number: '1.2-2' }} <span class="d-none d-md-inline symbol">BSQ</span>
</ng-template>
<ng-template #defaultTxType>
{{ calculateTotalOutput(tx.outputs) / 100 | number: '1.2-2' }} <span class="d-none d-md-inline symbol">BSQ</span>
</ng-template>
</td>
<td><app-time kind="since" [time]="tx.time / 1000" [fastRender]="true"></app-time></td>
<td class="d-none d-md-block"><a [routerLink]="['/block/' | relativeUrl, tx.blockHash]" [state]="{ data: { blockHeight: tx.blockHeight } }">{{ tx.blockHeight }}</a></td>
</tr>
</tbody>
</table>
<br>
<ngb-pagination class="pagination-container" *ngIf="transactions.value" [collectionSize]="transactions.value[1]" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="false"></ngb-pagination>
</ng-container>
</div>
<ng-template #loadingTmpl>
<tr *ngFor="let i of loadingItems">
<td *ngFor="let j of [1, 2, 3, 4, 5]"><span class="skeleton-loader"></span></td>
</tr>
</ng-template>

View File

@@ -1,23 +0,0 @@
label {
padding: 0.25rem 1rem;
white-space: nowrap;
}
:host ::ng-deep .dropdown-menu {
right: 0px;
left: inherit;
}
.pagination-container {
float: none;
@media(min-width: 400px){
float: right;
}
}
.container-xl {
padding-bottom: 60px;
@media(min-width: 400px){
padding-bottom: 100px;
}
}

View File

@@ -1,166 +0,0 @@
import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef, OnDestroy } from '@angular/core';
import { BisqTransaction, BisqOutput } from '../bisq.interfaces';
import { Observable, Subscription } from 'rxjs';
import { switchMap, map, tap } from 'rxjs/operators';
import { BisqApiService } from '../bisq-api.service';
import { SeoService } from '../../services/seo.service';
import { UntypedFormGroup, UntypedFormBuilder } from '@angular/forms';
import { Router, ActivatedRoute } from '@angular/router';
import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts } from '../../components/ngx-bootstrap-multiselect/types'
import { WebsocketService } from '../../services/websocket.service';
@Component({
selector: 'app-bisq-transactions',
templateUrl: './bisq-transactions.component.html',
styleUrls: ['./bisq-transactions.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BisqTransactionsComponent implements OnInit, OnDestroy {
transactions$: Observable<[BisqTransaction[], number]>;
page = 1;
itemsPerPage = 50;
fiveItemsPxSize = 250;
isLoading = true;
loadingItems: number[];
radioGroupForm: UntypedFormGroup;
types: string[] = [];
radioGroupSubscription: Subscription;
txTypeOptions: IMultiSelectOption[] = [
{ id: 1, name: $localize`Asset listing fee` },
{ id: 2, name: $localize`Blind vote` },
{ id: 3, name: $localize`Compensation request` },
{ id: 4, name: $localize`Genesis` },
{ id: 13, name: $localize`Irregular` },
{ id: 5, name: $localize`Lockup` },
{ id: 6, name: $localize`Pay trade fee` },
{ id: 7, name: $localize`Proof of burn` },
{ id: 8, name: $localize`Proposal` },
{ id: 9, name: $localize`Reimbursement request` },
{ id: 10, name: $localize`Transfer BSQ` },
{ id: 11, name: $localize`Unlock` },
{ id: 12, name: $localize`Vote reveal` },
];
txTypesDefaultChecked = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
txTypeDropdownSettings: IMultiSelectSettings = {
buttonClasses: 'btn btn-primary btn-sm',
displayAllSelectedText: true,
showCheckAll: true,
showUncheckAll: true,
maxHeight: '500px',
fixedTitle: true,
};
txTypeDropdownTexts: IMultiSelectTexts = {
defaultTitle: $localize`:@@bisq-transactions.filter:Filter`,
checkAll: $localize`:@@bisq-transactions.selectall:Select all`,
uncheckAll: $localize`:@@bisq-transactions.unselectall:Unselect all`,
};
// @ts-ignore
paginationSize: 'sm' | 'lg' = 'md';
paginationMaxSize = 5;
txTypes = ['ASSET_LISTING_FEE', 'BLIND_VOTE', 'COMPENSATION_REQUEST', 'GENESIS', 'LOCKUP', 'PAY_TRADE_FEE',
'PROOF_OF_BURN', 'PROPOSAL', 'REIMBURSEMENT_REQUEST', 'TRANSFER_BSQ', 'UNLOCK', 'VOTE_REVEAL', 'IRREGULAR'];
constructor(
private websocketService: WebsocketService,
private bisqApiService: BisqApiService,
private seoService: SeoService,
private formBuilder: UntypedFormBuilder,
private route: ActivatedRoute,
private router: Router,
private cd: ChangeDetectorRef,
) { }
ngOnInit(): void {
this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@add4cd82e3e38a3110fe67b3c7df56e9602644ee:Transactions`);
this.seoService.setDescription($localize`:@@meta.description.bisq.transactions:See recent BSQ transactions: amount, txid, associated Bitcoin block, transaction type, and more.`);
this.radioGroupForm = this.formBuilder.group({
txTypes: [this.txTypesDefaultChecked],
});
this.loadingItems = Array(this.itemsPerPage);
if (document.body.clientWidth < 670) {
this.paginationSize = 'sm';
this.paginationMaxSize = 3;
}
this.transactions$ = this.route.queryParams
.pipe(
tap((queryParams) => {
if (queryParams.page) {
const newPage = parseInt(queryParams.page, 10);
this.page = newPage;
} else {
this.page = 1;
}
if (queryParams.types) {
const types = queryParams.types.split(',').map((str: string) => parseInt(str, 10));
this.types = types.map((id: number) => this.txTypes[id - 1]);
this.radioGroupForm.get('txTypes').setValue(types, { emitEvent: false });
} else {
this.types = [];
this.radioGroupForm.get('txTypes').setValue([], { emitEvent: false });
}
this.cd.markForCheck();
}),
switchMap(() => this.bisqApiService.listTransactions$((this.page - 1) * this.itemsPerPage, this.itemsPerPage, this.types)),
map((response) => [response.body, parseInt(response.headers.get('x-total-count'), 10)])
);
this.radioGroupSubscription = this.radioGroupForm.valueChanges
.subscribe((data) => {
this.types = data.txTypes.map((id: number) => this.txTypes[id - 1]);
if (this.types.length === this.txTypes.length) {
this.types = [];
}
this.page = 1;
this.typesChanged(data.txTypes);
this.cd.markForCheck();
});
}
pageChange(page: number) {
this.router.navigate([], {
relativeTo: this.route,
queryParams: { page: page },
queryParamsHandling: 'merge',
});
}
typesChanged(types: number[]) {
this.router.navigate([], {
relativeTo: this.route,
queryParams: { types: types.join(','), page: 1 },
queryParamsHandling: 'merge',
});
}
calculateTotalOutput(outputs: BisqOutput[]): number {
return outputs.reduce((acc: number, output: BisqOutput) => acc + output.bsqAmount, 0);
}
getStringByTxType(type: string) {
const id = this.txTypes.indexOf(type) + 1;
return this.txTypeOptions.find((type) => id === type.id).name;
}
trackByFn(index: number) {
return index;
}
onResize(event: any) {
this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
}
ngOnDestroy(): void {
this.radioGroupSubscription.unsubscribe();
}
}

View File

@@ -1,83 +0,0 @@
<div class="header-bg box">
<div class="row">
<div class="col">
<table class="table table-borderless smaller-text table-xs" style="margin: 0;">
<tbody>
<ng-template ngFor let-input [ngForOf]="tx.inputs" [ngForTrackBy]="trackByIndexFn">
<tr *ngIf="input.isVerified">
<td class="arrow-td">
<ng-template [ngIf]="input.spendingTxId === null" [ngIfElse]="hasPreoutput">
<span class="grey">
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
</span>
</ng-template>
<ng-template #hasPreoutput>
<a [routerLink]="['/tx/' | relativeUrl, input.spendingTxId]" class="red">
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
</a>
</ng-template>
</td>
<td>
<a [routerLink]="['/address/' | relativeUrl, 'B' + input.address]" title="B{{ input.address }}">
<span class="d-block d-lg-none">B{{ input.address | shortenString : 16 }}</span>
<span class="d-none d-lg-block">B{{ input.address | shortenString : 35 }}</span>
</a>
</td>
<td class="text-right nowrap">
<app-bsq-amount [bsq]="input.bsqAmount"></app-bsq-amount>
</td>
</tr>
</ng-template>
</tbody>
</table>
</div>
<div class="w-100 d-block d-md-none"></div>
<div class="col mobile-bottomcol">
<table class="table table-borderless smaller-text table-xs" style="margin: 0;">
<tbody>
<ng-template ngFor let-output [ngForOf]="tx.outputs" [ngForTrackBy]="trackByIndexFn">
<tr *ngIf="output.isVerified && output.opReturn === undefined">
<td>
<a [routerLink]="['/address/' | relativeUrl, 'B' + output.address]" title="B{{ output.address }}">
<span class="d-block d-lg-none">B{{ output.address | shortenString : 16 }}</span>
<span class="d-none d-lg-block">B{{ output.address | shortenString : 35 }}</span>
</a>
</td>
<td class="text-right nowrap">
<app-bsq-amount [bsq]="output.bsqAmount"></app-bsq-amount>
</td>
<td class="arrow-td">
<span *ngIf="!output.spentInfo; else spent" class="green">
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
</span>
<ng-template #spent>
<a [routerLink]="['/tx/' | relativeUrl, output.spentInfo.txId]" class="red">
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
</a>
</ng-template>
</td>
</tr>
</ng-template>
</tbody>
</table>
</div>
</div>
<div>
<div class="transaction-fee" *ngIf="showConfirmations && tx.burntFee">
<ng-container i18n="BSQ burnt amount">Burnt amount</ng-container>: {{ tx.burntFee / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span> <span class="extra-info"><span class="fiat"><app-bsq-amount [bsq]="tx.burntFee" [forceFiat]="true" [green]="true"></app-bsq-amount></span></span>
</div>
<div class="btn-container">
<span *ngIf="showConfirmations && latestBlock$ | async as latestBlock">
<app-confirmations [chainTip]="latestBlock?.height" [height]="tx.blockHeight" [hideUnconfirmed]="true" buttonClass="mt-2"></app-confirmations>
&nbsp;
</span>
<button type="button" class="btn btn-sm btn-primary mt-2" (click)="switchCurrency()">
<app-bsq-amount [bsq]="totalOutput"></app-bsq-amount>
</button>
</div>
<div class="clearfix"></div>
</div>
</div>

View File

@@ -1,103 +0,0 @@
.arrow-td {
width: 20px;
}
.green, .grey, .red {
font-size: 16px;
top: -2px;
position: relative;
@media( min-width: 576px){
font-size: 19px;
}
}
.green {
color:#28a745;
}
.red {
color:#dc3545;
}
.grey {
color:#6c757d;
}
@media (max-width: 767.98px) {
.mobile-bottomcol {
margin-top: 15px;
}
.details-table td:first-child {
white-space: pre-wrap;
}
}
.details-table {
margin-top: 5px;
}
.details-table td {
padding: 0.75rem;
}
.details-table td:nth-child(2) {
word-break: break-all;
white-space: normal;
font-family: "Courier New", Courier, monospace;
font-size: 12px;
}
.smaller-text {
font-size: 12px;
@media (min-width: 576px) {
font-size: 14px !important;
}
}
.longer {
max-width: 100% !important;
width: 200px;
display: inline-block;
}
.row{
flex-direction: column;
@media (min-width: 992px) {
flex-direction: row;
}
}
.extra-info {
display: inline-table;
.fiat {
font-size: 14px;
display: block;
text-align: right;
}
}
.transaction-fee {
display: block;
margin: 0px auto 5px;
@media (min-width: 576px) {
display: inline-table;
}
}
.fiat {
margin-left: 10px;
font-size: 13px;
@media (min-width: 576px) {
font-size: 14px;
}
}
.btn-container {
text-align: right;
@media (min-width: 576px) {
display: inline-table;
float: right;
}
}

View File

@@ -1,42 +0,0 @@
import { Component, OnInit, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core';
import { BisqTransaction } from '../../bisq/bisq.interfaces';
import { StateService } from '../../services/state.service';
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { Block } from '../../interfaces/electrs.interface';
@Component({
selector: 'app-bisq-transfers',
templateUrl: './bisq-transfers.component.html',
styleUrls: ['./bisq-transfers.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BisqTransfersComponent implements OnInit, OnChanges {
@Input() tx: BisqTransaction;
@Input() showConfirmations = false;
totalOutput: number;
latestBlock$: Observable<Block>;
constructor(
private stateService: StateService,
) { }
trackByIndexFn(index: number) {
return index;
}
ngOnInit() {
this.latestBlock$ = this.stateService.blocks$.pipe(map((blocks) => blocks[0]));
}
ngOnChanges() {
this.totalOutput = this.tx.outputs.filter((output) => output.isVerified).reduce((acc, output) => acc + output.bsqAmount, 0);
}
switchCurrency() {
const oldvalue = !this.stateService.viewFiat$.value;
this.stateService.viewFiat$.next(oldvalue);
}
}

View File

@@ -1,261 +0,0 @@
export interface BisqBlocks {
chainHeight: number;
blocks: BisqBlock[];
}
export interface BisqBlock {
height: number;
time: number;
hash: string;
previousBlockHash: string;
txs: BisqTransaction[];
}
export interface BisqTransaction {
txVersion: string;
id: string;
blockHeight: number;
blockHash: string;
time: number;
inputs: BisqInput[];
outputs: BisqOutput[];
txType: string;
txTypeDisplayString: string;
burntFee: number;
invalidatedBsq: number;
unlockBlockHeight: number;
}
interface BisqInput {
spendingTxOutputIndex: number;
spendingTxId: string;
bsqAmount: number;
isVerified: boolean;
address: string;
time: number;
}
export interface BisqOutput {
txVersion: string;
txId: string;
index: number;
bsqAmount: number;
btcAmount: number;
height: number;
isVerified: boolean;
burntFee: number;
invalidatedBsq: number;
address: string;
scriptPubKey: BisqScriptPubKey;
spentInfo?: SpentInfo;
time: any;
txType: string;
txTypeDisplayString: string;
txOutputType: string;
txOutputTypeDisplayString: string;
lockTime: number;
isUnspent: boolean;
opReturn?: string;
}
export interface BisqStats {
minted: number;
burnt: number;
addresses: number;
unspent_txos: number;
spent_txos: number;
}
interface BisqScriptPubKey {
addresses: string[];
asm: string;
hex: string;
reqSigs?: number;
type: string;
}
interface SpentInfo {
height: number;
inputIndex: number;
txId: string;
}
export interface BisqTrade {
direction: string;
price: string;
amount: string;
volume: string;
payment_method: string;
trade_id: string;
trade_date: number;
market?: string;
}
export interface Currencies { [txid: string]: Currency; }
export interface Currency {
code: string;
name: string;
precision: number;
_type: string;
}
export interface Depth { [market: string]: Market; }
interface Market {
'buys': string[];
'sells': string[];
}
export interface HighLowOpenClose {
period_start: number | string;
open: string;
high: string;
low: string;
close: string;
volume_left: string;
volume_right: string;
avg: string;
}
export interface Markets { [txid: string]: Pair; }
interface Pair {
pair: string;
lname: string;
rname: string;
lsymbol: string;
rsymbol: string;
lprecision: number;
rprecision: number;
ltype: string;
rtype: string;
name: string;
}
export interface Offers { [market: string]: OffersMarket; }
export interface OffersMarket {
buys: Offer[] | null;
sells: Offer[] | null;
}
export interface OffersData {
direction: string;
currencyCode: string;
minAmount: number;
amount: number;
price: number;
date: number;
useMarketBasedPrice: boolean;
marketPriceMargin: number;
paymentMethod: string;
id: string;
currencyPair: string;
primaryMarketDirection: string;
priceDisplayString: string;
primaryMarketAmountDisplayString: string;
primaryMarketMinAmountDisplayString: string;
primaryMarketVolumeDisplayString: string;
primaryMarketMinVolumeDisplayString: string;
primaryMarketPrice: number;
primaryMarketAmount: number;
primaryMarketMinAmount: number;
primaryMarketVolume: number;
primaryMarketMinVolume: number;
}
export interface Offer {
offer_id: string;
offer_date: number;
direction: string;
min_amount: string;
amount: string;
price: string;
volume: string;
payment_method: string;
offer_fee_txid: any;
}
export interface Tickers { [market: string]: Ticker | null; }
export interface Ticker {
last: string;
high: string;
low: string;
volume_left: string;
volume_right: string;
buy: string | null;
sell: string | null;
}
export interface Trade {
market?: string;
price: string;
amount: string;
volume: string;
payment_method: string;
trade_id: string;
trade_date: number;
_market: Pair;
}
export interface TradesData {
currency: string;
direction: string;
tradePrice: number;
tradeAmount: number;
tradeDate: number;
paymentMethod: string;
offerDate: number;
useMarketBasedPrice: boolean;
marketPriceMargin: number;
offerAmount: number;
offerMinAmount: number;
offerId: string;
depositTxId?: string;
currencyPair: string;
primaryMarketDirection: string;
primaryMarketTradePrice: number;
primaryMarketTradeAmount: number;
primaryMarketTradeVolume: number;
_market: string;
_tradePriceStr: string;
_tradeAmountStr: string;
_tradeVolumeStr: string;
_offerAmountStr: string;
_tradePrice: number;
_tradeAmount: number;
_tradeVolume: number;
_offerAmount: number;
}
export interface MarketVolume {
period_start: number;
num_trades: number;
volume: string;
}
export interface MarketsApiError {
success: number;
error: string;
}
export type Interval = 'minute' | 'half_hour' | 'hour' | 'half_day' | 'day' | 'week' | 'month' | 'year' | 'auto';
export interface SummarizedIntervals { [market: string]: SummarizedInterval; }
export interface SummarizedInterval {
period_start: number;
open: number;
close: number;
high: number;
low: number;
avg: number;
volume_right: number;
volume_left: number;
time?: number;
}

View File

@@ -1,84 +0,0 @@
import { NgModule } from '@angular/core';
import { BisqRoutingModule } from './bisq.routing.module';
import { SharedModule } from '../shared/shared.module';
import { LightweightChartsComponent } from './lightweight-charts/lightweight-charts.component';
import { LightweightChartsAreaComponent } from './lightweight-charts-area/lightweight-charts-area.component';
import { BisqMarketComponent } from './bisq-market/bisq-market.component';
import { BisqTransactionsComponent } from './bisq-transactions/bisq-transactions.component';
import { BisqTransactionComponent } from './bisq-transaction/bisq-transaction.component';
import { BisqBlockComponent } from './bisq-block/bisq-block.component';
import { BisqDashboardComponent } from './bisq-dashboard/bisq-dashboard.component';
import { BisqMainDashboardComponent } from './bisq-main-dashboard/bisq-main-dashboard.component';
import { BisqIconComponent } from './bisq-icon/bisq-icon.component';
import { BisqTransactionDetailsComponent } from './bisq-transaction-details/bisq-transaction-details.component';
import { BisqTransfersComponent } from './bisq-transfers/bisq-transfers.component';
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
import { faLeaf, faQuestion, faExclamationTriangle, faRocket, faRetweet, faFileAlt, faMoneyBill,
faEye, faEyeSlash, faLock, faLockOpen, faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
import { BisqBlocksComponent } from './bisq-blocks/bisq-blocks.component';
import { BisqApiService } from './bisq-api.service';
import { BisqAddressComponent } from './bisq-address/bisq-address.component';
import { BisqStatsComponent } from './bisq-stats/bisq-stats.component';
import { BsqAmountComponent } from './bsq-amount/bsq-amount.component';
import { BisqTradesComponent } from './bisq-trades/bisq-trades.component';
import { CommonModule } from '@angular/common';
import { AutofocusDirective } from '../components/ngx-bootstrap-multiselect/autofocus.directive';
import { MultiSelectSearchFilter } from '../components/ngx-bootstrap-multiselect/search-filter.pipe';
import { OffClickDirective } from '../components/ngx-bootstrap-multiselect/off-click.directive';
import { NgxDropdownMultiselectComponent } from '../components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component';
import { BisqMasterPageComponent } from '../components/bisq-master-page/bisq-master-page.component';
@NgModule({
declarations: [
BisqMasterPageComponent,
BisqTransactionsComponent,
BisqTransactionComponent,
BisqBlockComponent,
BisqTransactionComponent,
BisqIconComponent,
BisqTransactionDetailsComponent,
BisqTransfersComponent,
BisqBlocksComponent,
BisqAddressComponent,
BisqStatsComponent,
BsqAmountComponent,
LightweightChartsComponent,
LightweightChartsAreaComponent,
BisqDashboardComponent,
BisqMarketComponent,
BisqTradesComponent,
BisqMainDashboardComponent,
NgxDropdownMultiselectComponent,
AutofocusDirective,
OffClickDirective,
],
imports: [
CommonModule,
BisqRoutingModule,
SharedModule,
FontAwesomeModule,
],
providers: [
BisqApiService,
MultiSelectSearchFilter,
AutofocusDirective,
OffClickDirective,
]
})
export class BisqModule {
constructor(library: FaIconLibrary) {
library.addIcons(faQuestion);
library.addIcons(faExclamationCircle);
library.addIcons(faExclamationTriangle);
library.addIcons(faRocket);
library.addIcons(faRetweet);
library.addIcons(faLeaf);
library.addIcons(faFileAlt);
library.addIcons(faMoneyBill);
library.addIcons(faEye);
library.addIcons(faEyeSlash);
library.addIcons(faLock);
library.addIcons(faLockOpen);
}
}

Some files were not shown because too many files have changed in this diff Show More