mirror of
https://github.com/benjamin-wilson/public-pool.git
synced 2026-09-29 17:15:03 -07:00
Compare commits
34
Commits
master
...
87508bfb69
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87508bfb69 | ||
|
|
39e537a00c | ||
|
|
9ecfc68155 | ||
|
|
3e2de62259 | ||
|
|
8e977c21a1 | ||
|
|
5fd9f98807 | ||
|
|
6bc42defef | ||
|
|
bbc14e2272 | ||
|
|
fc767fa770 | ||
|
|
40e32d6e1a | ||
|
|
df6ec07c58 | ||
|
|
df245f396e | ||
|
|
42fc863c7c | ||
|
|
e46bb24155 | ||
|
|
cb4a173733 | ||
|
|
83c769849d | ||
|
|
ee5fea1df5 | ||
|
|
06acad6770 | ||
|
|
f7b22bb310 | ||
|
|
6cf6a007d9 | ||
|
|
5b71632f8a | ||
|
|
c5ff8521ac | ||
|
|
53ee510a1e | ||
|
|
7be554a358 | ||
|
|
eb5d434157 | ||
|
|
893c7c2003 | ||
|
|
4736eadf7b | ||
|
|
1089e6210c | ||
|
|
017978d250 | ||
|
|
5a17528b8f | ||
|
|
0b147ea5db | ||
|
|
bffcf6c74c | ||
|
|
96a38f9d9b | ||
|
|
39c4c033e2 |
@@ -38,3 +38,12 @@ ENABLE_PROXY=true
|
||||
BRAIINS_ACCESS_TOKEN=
|
||||
|
||||
PROXY_PORT=3333
|
||||
|
||||
#postgresql
|
||||
DB_HOST=
|
||||
DB_PORT=
|
||||
DB_USERNAME=
|
||||
DB_PASSWORD=
|
||||
DB_DATABASE=
|
||||
|
||||
PRODUCTION=false
|
||||
Generated
+240
@@ -29,6 +29,7 @@
|
||||
"discord.js": "^14.11.0",
|
||||
"merkle-lib": "^2.0.10",
|
||||
"node-telegram-bot-api": "^0.61.0",
|
||||
"pg": "^8.11.3",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rpc-bitcoin": "^2.0.0",
|
||||
"rxjs": "^7.2.0",
|
||||
@@ -3740,6 +3741,14 @@
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/buffer-writer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
|
||||
"integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-xor": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
|
||||
@@ -8656,6 +8665,11 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/packet-reader": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
|
||||
"integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ=="
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
@@ -8799,6 +8813,89 @@
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="
|
||||
},
|
||||
"node_modules/pg": {
|
||||
"version": "8.11.3",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz",
|
||||
"integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==",
|
||||
"dependencies": {
|
||||
"buffer-writer": "2.0.0",
|
||||
"packet-reader": "1.0.0",
|
||||
"pg-connection-string": "^2.6.2",
|
||||
"pg-pool": "^3.6.1",
|
||||
"pg-protocol": "^1.6.0",
|
||||
"pg-types": "^2.1.0",
|
||||
"pgpass": "1.x"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"pg-cloudflare": "^1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pg-native": ">=3.0.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"pg-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pg-cloudflare": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz",
|
||||
"integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/pg-connection-string": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz",
|
||||
"integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA=="
|
||||
},
|
||||
"node_modules/pg-int8": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-pool": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz",
|
||||
"integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==",
|
||||
"peerDependencies": {
|
||||
"pg": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-protocol": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz",
|
||||
"integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q=="
|
||||
},
|
||||
"node_modules/pg-types": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
||||
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
||||
"dependencies": {
|
||||
"pg-int8": "1.0.1",
|
||||
"postgres-array": "~2.0.0",
|
||||
"postgres-bytea": "~1.0.0",
|
||||
"postgres-date": "~1.0.4",
|
||||
"postgres-interval": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/pgpass": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
||||
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
|
||||
"dependencies": {
|
||||
"split2": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
@@ -8934,6 +9031,41 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-array": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-bytea": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
|
||||
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-date": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
||||
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-interval": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
||||
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
||||
"dependencies": {
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@@ -11324,6 +11456,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||
"engines": {
|
||||
"node": ">=0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
@@ -14264,6 +14404,11 @@
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"dev": true
|
||||
},
|
||||
"buffer-writer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
|
||||
"integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw=="
|
||||
},
|
||||
"buffer-xor": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
|
||||
@@ -17987,6 +18132,11 @@
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"dev": true
|
||||
},
|
||||
"packet-reader": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
|
||||
"integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ=="
|
||||
},
|
||||
"parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
@@ -18094,6 +18244,68 @@
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="
|
||||
},
|
||||
"pg": {
|
||||
"version": "8.11.3",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz",
|
||||
"integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==",
|
||||
"requires": {
|
||||
"buffer-writer": "2.0.0",
|
||||
"packet-reader": "1.0.0",
|
||||
"pg-cloudflare": "^1.1.1",
|
||||
"pg-connection-string": "^2.6.2",
|
||||
"pg-pool": "^3.6.1",
|
||||
"pg-protocol": "^1.6.0",
|
||||
"pg-types": "^2.1.0",
|
||||
"pgpass": "1.x"
|
||||
}
|
||||
},
|
||||
"pg-cloudflare": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz",
|
||||
"integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==",
|
||||
"optional": true
|
||||
},
|
||||
"pg-connection-string": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz",
|
||||
"integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA=="
|
||||
},
|
||||
"pg-int8": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="
|
||||
},
|
||||
"pg-pool": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz",
|
||||
"integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==",
|
||||
"requires": {}
|
||||
},
|
||||
"pg-protocol": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz",
|
||||
"integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q=="
|
||||
},
|
||||
"pg-types": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
||||
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
||||
"requires": {
|
||||
"pg-int8": "1.0.1",
|
||||
"postgres-array": "~2.0.0",
|
||||
"postgres-bytea": "~1.0.0",
|
||||
"postgres-date": "~1.0.4",
|
||||
"postgres-interval": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"pgpass": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
||||
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
|
||||
"requires": {
|
||||
"split2": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
@@ -18198,6 +18410,29 @@
|
||||
"integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
|
||||
"dev": true
|
||||
},
|
||||
"postgres-array": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="
|
||||
},
|
||||
"postgres-bytea": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
|
||||
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="
|
||||
},
|
||||
"postgres-date": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
||||
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="
|
||||
},
|
||||
"postgres-interval": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
||||
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
||||
"requires": {
|
||||
"xtend": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@@ -19836,6 +20071,11 @@
|
||||
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
|
||||
"requires": {}
|
||||
},
|
||||
"xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
|
||||
},
|
||||
"y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"discord.js": "^14.11.0",
|
||||
"merkle-lib": "^2.0.10",
|
||||
"node-telegram-bot-api": "^0.61.0",
|
||||
"pg": "^8.11.3",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rpc-bitcoin": "^2.0.0",
|
||||
"rxjs": "^7.2.0",
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class UniqueNonceIndex implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX "IDX_unique_nonce" ON "client_entity" ("sessionId") WHERE "deletedAt" IS NOT NULL`
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP INDEX "IDX_unique_nonce"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { UserAgentReportService } from './user-agent-report.service';
|
||||
import { UserAgentReportView } from './user-agent-report.view';
|
||||
|
||||
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([UserAgentReportView])],
|
||||
providers: [UserAgentReportService],
|
||||
exports: [TypeOrmModule, UserAgentReportService],
|
||||
})
|
||||
export class UserAgentReportModule { }
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { UserAgentReportView } from './user-agent-report.view';
|
||||
|
||||
@Injectable()
|
||||
export class UserAgentReportService {
|
||||
|
||||
constructor(
|
||||
@InjectRepository(UserAgentReportView)
|
||||
private userAgentReport: Repository<UserAgentReportView>,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
public async getReport() {
|
||||
return await this.userAgentReport.find();
|
||||
}
|
||||
|
||||
public async refreshReport() {
|
||||
try {
|
||||
return await this.userAgentReport.query(`
|
||||
COMMIT;
|
||||
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
|
||||
REFRESH MATERIALIZED VIEW user_agent_report_view;
|
||||
COMMIT;
|
||||
`);
|
||||
} catch (e) {
|
||||
|
||||
console.log(e)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { DataSource, ViewColumn, ViewEntity } from 'typeorm';
|
||||
|
||||
import { ClientEntity } from '../../client/client.entity';
|
||||
|
||||
@ViewEntity({
|
||||
materialized: true,
|
||||
expression: (dataSource: DataSource) =>
|
||||
dataSource
|
||||
.createQueryBuilder()
|
||||
.select('client.userAgent as "userAgent"')
|
||||
.addSelect('COUNT(client.userAgent)', 'count')
|
||||
.addSelect('MAX(client.bestDifficulty)', 'bestDifficulty')
|
||||
.addSelect('SUM(client.hashRate)', 'totalHashRate')
|
||||
.from(ClientEntity, 'client')
|
||||
.groupBy('client.userAgent')
|
||||
.orderBy('"totalHashRate"', 'DESC')
|
||||
})
|
||||
export class UserAgentReportView {
|
||||
@ViewColumn()
|
||||
userAgent: string;
|
||||
@ViewColumn()
|
||||
count: number;
|
||||
@ViewColumn()
|
||||
bestDifficulty: number;
|
||||
@ViewColumn()
|
||||
totalHashRate: number;
|
||||
}
|
||||
@@ -11,9 +11,12 @@ export class AddressSettingsEntity extends TrackedEntity {
|
||||
@Column({ default: 0 })
|
||||
shares: number;
|
||||
|
||||
@Column({ type: 'real', default: 0 })
|
||||
@Column({ type: 'decimal', default: 0 })
|
||||
bestDifficulty: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
bestDifficultyUserAgent: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
miscCoinbaseScriptData: string;
|
||||
|
||||
|
||||
@@ -17,13 +17,18 @@ export class AddressSettingsService {
|
||||
public async getSettings(address: string, createIfNotFound: boolean) {
|
||||
const settings = await this.addressSettingsRepository.findOne({ where: { address } });
|
||||
if (createIfNotFound == true && settings == null) {
|
||||
// It's possible to have a race condition here so if we get a PK violation, fetch it
|
||||
try {
|
||||
return await this.createNew(address);
|
||||
} catch (e) {
|
||||
return await this.addressSettingsRepository.findOne({ where: { address } });
|
||||
}
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
public async updateBestDifficulty(address: string, bestDifficulty: number) {
|
||||
return await this.addressSettingsRepository.update({ address }, { bestDifficulty });
|
||||
public async updateBestDifficulty(address: string, bestDifficulty: number, bestDifficultyUserAgent: string) {
|
||||
return await this.addressSettingsRepository.update({ address }, { bestDifficulty, bestDifficultyUserAgent });
|
||||
}
|
||||
|
||||
public async createNew(address: string) {
|
||||
@@ -46,4 +51,12 @@ export class AddressSettingsService {
|
||||
bestDifficulty: 0
|
||||
});
|
||||
}
|
||||
|
||||
public async getHighScores() {
|
||||
return await this.addressSettingsRepository.createQueryBuilder()
|
||||
.select('"updatedAt", "bestDifficulty", "bestDifficultyUserAgent"')
|
||||
.orderBy('"bestDifficulty"', 'DESC')
|
||||
.limit(10)
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { Column, Entity, Index, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
import { ClientEntity } from '../client/client.entity';
|
||||
import { TrackedEntity } from '../utils/TrackedEntity.entity';
|
||||
|
||||
@Entity()
|
||||
//Index for getHashRateForSession
|
||||
@Index(["address", "clientName", "sessionId"])
|
||||
//Index for statistics save
|
||||
@Index(["address", "clientName", "sessionId", "time"])
|
||||
@Index(["clientId", "time"])
|
||||
export class ClientStatisticsEntity extends TrackedEntity {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
@@ -22,14 +21,27 @@ export class ClientStatisticsEntity extends TrackedEntity {
|
||||
sessionId: string;
|
||||
|
||||
@Index()
|
||||
@Column({ type: 'integer' })
|
||||
@Column({ type: 'bigint' })
|
||||
time: number;
|
||||
|
||||
@Column({ type: 'real' })
|
||||
@Column({ type: 'decimal' })
|
||||
shares: number;
|
||||
|
||||
@Column({ default: 0, type: 'integer' })
|
||||
@Column({ default: 0, type: 'bigint' })
|
||||
acceptedCount: number;
|
||||
|
||||
|
||||
|
||||
@ManyToOne(
|
||||
() => ClientEntity,
|
||||
clientEntity => clientEntity.statistics,
|
||||
{ nullable: false, }
|
||||
)
|
||||
client: ClientEntity;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'clientId' })
|
||||
public clientId: string;
|
||||
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import { ClientStatisticsEntity } from './client-statistics.entity';
|
||||
|
||||
@@ -10,7 +10,8 @@ export class ClientStatisticsService {
|
||||
|
||||
constructor(
|
||||
|
||||
|
||||
@InjectDataSource()
|
||||
private dataSource: DataSource,
|
||||
@InjectRepository(ClientStatisticsEntity)
|
||||
private clientStatisticsRepository: Repository<ClientStatisticsEntity>,
|
||||
) {
|
||||
@@ -18,28 +19,19 @@ export class ClientStatisticsService {
|
||||
}
|
||||
|
||||
public async save(clientStatistic: Partial<ClientStatisticsEntity>) {
|
||||
// Attempt to update the existing record
|
||||
const updateResult = await this.clientStatisticsRepository
|
||||
.createQueryBuilder()
|
||||
.update(ClientStatisticsEntity)
|
||||
.set({
|
||||
shares: () => `"shares" + :sharesIncrement`,
|
||||
acceptedCount: () => `"acceptedCount" + 1`
|
||||
// // Attempt to update the existing record
|
||||
const result = await this.clientStatisticsRepository.update({ clientId: clientStatistic.clientId, time: clientStatistic.time },
|
||||
{
|
||||
shares: clientStatistic.shares,
|
||||
acceptedCount: clientStatistic.acceptedCount
|
||||
})
|
||||
.where('address = :address AND clientName = :clientName AND sessionId = :sessionId AND time = :time', {
|
||||
address: clientStatistic.address,
|
||||
clientName: clientStatistic.clientName,
|
||||
sessionId: clientStatistic.sessionId,
|
||||
time: clientStatistic.time,
|
||||
sharesIncrement: clientStatistic.shares
|
||||
})
|
||||
.execute();
|
||||
|
||||
// Check if the update affected any rows
|
||||
if (updateResult.affected === 0) {
|
||||
if (result.affected === 0) {
|
||||
// If no rows were updated, insert a new record
|
||||
await this.clientStatisticsRepository.insert(clientStatistic);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async deleteOldStatistics() {
|
||||
@@ -53,57 +45,7 @@ export class ClientStatisticsService {
|
||||
.execute();
|
||||
}
|
||||
|
||||
public async getChartDataForSite() {
|
||||
|
||||
var yesterday = new Date(new Date().getTime() - (24 * 60 * 60 * 1000));
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
time AS label,
|
||||
ROUND(((SUM(shares) * 4294967296) / 600)) AS data
|
||||
FROM
|
||||
client_statistics_entity AS entry
|
||||
WHERE
|
||||
entry.time > ${yesterday.getTime()}
|
||||
GROUP BY
|
||||
time
|
||||
ORDER BY
|
||||
time
|
||||
LIMIT 144;
|
||||
|
||||
`;
|
||||
|
||||
const result: any[] = await this.clientStatisticsRepository.query(query);
|
||||
|
||||
|
||||
return result.map(res => {
|
||||
res.label = new Date(res.label).toISOString();
|
||||
return res;
|
||||
}).slice(0, result.length - 1)
|
||||
|
||||
}
|
||||
|
||||
|
||||
// public async getHashRateForAddress(address: string) {
|
||||
|
||||
// const oneHour = new Date(new Date().getTime() - (60 * 60 * 1000));
|
||||
|
||||
// const query = `
|
||||
// SELECT
|
||||
// SUM(entry.shares) AS difficultySum
|
||||
// FROM
|
||||
// client_statistics_entity AS entry
|
||||
// WHERE
|
||||
// entry.address = ? AND entry.time > ${oneHour}
|
||||
// `;
|
||||
|
||||
// const result = await this.clientStatisticsRepository.query(query, [address]);
|
||||
|
||||
// const difficultySum = result[0].difficultySum;
|
||||
|
||||
// return (difficultySum * 4294967296) / (600);
|
||||
|
||||
// }
|
||||
|
||||
public async getChartDataForAddress(address: string) {
|
||||
|
||||
@@ -111,12 +53,12 @@ export class ClientStatisticsService {
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
time label,
|
||||
time AS label,
|
||||
(SUM(shares) * 4294967296) / 600 AS data
|
||||
FROM
|
||||
client_statistics_entity AS entry
|
||||
WHERE
|
||||
entry.address = ? AND entry.time > ${yesterday.getTime()}
|
||||
entry.address = $1 AND entry.time > $2
|
||||
GROUP BY
|
||||
time
|
||||
ORDER BY
|
||||
@@ -125,10 +67,10 @@ export class ClientStatisticsService {
|
||||
|
||||
`;
|
||||
|
||||
const result = await this.clientStatisticsRepository.query(query, [address]);
|
||||
const result = await this.clientStatisticsRepository.query(query, [address, yesterday.getTime()]);
|
||||
|
||||
return result.map(res => {
|
||||
res.label = new Date(res.label).toISOString();
|
||||
res.label = new Date(parseInt(res.label)).toISOString();
|
||||
return res;
|
||||
}).slice(0, result.length - 1);
|
||||
|
||||
@@ -146,7 +88,7 @@ export class ClientStatisticsService {
|
||||
FROM
|
||||
client_statistics_entity AS entry
|
||||
WHERE
|
||||
entry.address = ? AND entry.clientName = ? AND entry.time > ${oneHour.getTime()}
|
||||
entry.address = $1 AND entry.clientName = $2 AND entry.time > ${oneHour.getTime()}
|
||||
`;
|
||||
|
||||
const result = await this.clientStatisticsRepository.query(query, [address, clientName]);
|
||||
@@ -163,12 +105,12 @@ export class ClientStatisticsService {
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
time label,
|
||||
time AS label,
|
||||
(SUM(shares) * 4294967296) / 600 AS data
|
||||
FROM
|
||||
client_statistics_entity AS entry
|
||||
WHERE
|
||||
entry.address = ? AND entry.clientName = ? AND entry.time > ${yesterday.getTime()}
|
||||
entry.address = $1 AND entry."clientName" = $2 AND entry.time > ${yesterday.getTime()}
|
||||
GROUP BY
|
||||
time
|
||||
ORDER BY
|
||||
@@ -179,7 +121,7 @@ export class ClientStatisticsService {
|
||||
const result = await this.clientStatisticsRepository.query(query, [address, clientName]);
|
||||
|
||||
return result.map(res => {
|
||||
res.label = new Date(res.label).toISOString();
|
||||
res.label = new Date(parseInt(res.label)).toISOString();
|
||||
return res;
|
||||
}).slice(0, result.length - 1);
|
||||
|
||||
@@ -187,22 +129,22 @@ export class ClientStatisticsService {
|
||||
}
|
||||
|
||||
|
||||
public async getHashRateForSession(address: string, clientName: string, sessionId: string) {
|
||||
public async getHashRateForSession(clientId: string) {
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
createdAt,
|
||||
updatedAt,
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
shares
|
||||
FROM
|
||||
client_statistics_entity AS entry
|
||||
WHERE
|
||||
entry.address = ? AND entry.clientName = ? AND entry.sessionId = ?
|
||||
entry."clientId" = $1
|
||||
ORDER BY time DESC
|
||||
LIMIT 2;
|
||||
`;
|
||||
|
||||
const result = await this.clientStatisticsRepository.query(query, [address, clientName, sessionId]);
|
||||
const result = await this.clientStatisticsRepository.query(query, [clientId]);
|
||||
|
||||
if (result.length < 1) {
|
||||
return 0;
|
||||
@@ -215,29 +157,29 @@ export class ClientStatisticsService {
|
||||
if (time < 1) {
|
||||
return 0;
|
||||
}
|
||||
return (latestStat.shares * 4294967296) / (time / 1000);
|
||||
return (parseFloat(latestStat.shares) * 4294967296) / (time / 1000);
|
||||
} else {
|
||||
const secondLatestStat = result[1];
|
||||
const time = new Date(latestStat.updatedAt).getTime() - new Date(secondLatestStat.createdAt).getTime();
|
||||
if (time < 1) {
|
||||
return 0;
|
||||
}
|
||||
return ((latestStat.shares + secondLatestStat.shares) * 4294967296) / (time / 1000);
|
||||
return ((parseFloat(latestStat.shares) + parseFloat(secondLatestStat.shares)) * 4294967296) / (time / 1000);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async getChartDataForSession(address: string, clientName: string, sessionId: string) {
|
||||
public async getChartDataForSession(clientId: string) {
|
||||
var yesterday = new Date(new Date().getTime() - (24 * 60 * 60 * 1000));
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
time label,
|
||||
time AS label,
|
||||
(SUM(shares) * 4294967296) / 600 AS data
|
||||
FROM
|
||||
client_statistics_entity AS entry
|
||||
WHERE
|
||||
entry.address = ? AND entry.clientName = ? AND entry.sessionId = ? AND entry.time > ${yesterday.getTime()}
|
||||
entry."clientId" = $1 AND entry.time > ${yesterday.getTime()}
|
||||
GROUP BY
|
||||
time
|
||||
ORDER BY
|
||||
@@ -245,10 +187,10 @@ export class ClientStatisticsService {
|
||||
LIMIT 144;
|
||||
`;
|
||||
|
||||
const result = await this.clientStatisticsRepository.query(query, [address, clientName, sessionId]);
|
||||
const result = await this.clientStatisticsRepository.query(query, [clientId]);
|
||||
|
||||
return result.map(res => {
|
||||
res.label = new Date(res.label).toISOString();
|
||||
res.label = new Date(parseInt(res.label)).toISOString();
|
||||
return res;
|
||||
}).slice(0, result.length - 1);
|
||||
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
|
||||
import { Column, Entity, Index, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
import { DateTimeTransformer } from '../utils/DateTimeTransformer';
|
||||
import { ClientStatisticsEntity } from '../client-statistics/client-statistics.entity';
|
||||
import { TrackedEntity } from '../utils/TrackedEntity.entity';
|
||||
|
||||
//https://www.sqlite.org/withoutrowid.html
|
||||
|
||||
//The WITHOUT ROWID optimization is likely to be helpful for tables that have non-integer
|
||||
// or composite (multi-column) PRIMARY KEYs and that do not store large strings or BLOBs.
|
||||
//WITHOUT ROWID tables work best when individual rows are not too large.
|
||||
@Entity({ withoutRowid: true })
|
||||
@Index(['address', 'clientName', 'sessionId'], { unique: true })
|
||||
@Entity()
|
||||
@Index("IDX_unique_nonce", { synchronize: false })
|
||||
export class ClientEntity extends TrackedEntity {
|
||||
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@PrimaryColumn({ length: 62, type: 'varchar' })
|
||||
@Index()
|
||||
@Column({ length: 62, type: 'varchar' })
|
||||
address: string;
|
||||
|
||||
@PrimaryColumn({ length: 64, type: 'varchar' })
|
||||
@Column({ length: 64, type: 'varchar' })
|
||||
clientName: string;
|
||||
|
||||
@PrimaryColumn({ length: 8, type: 'varchar' })
|
||||
@Column({ length: 8, type: 'varchar', })
|
||||
sessionId: string;
|
||||
|
||||
|
||||
@@ -27,15 +26,20 @@ export class ClientEntity extends TrackedEntity {
|
||||
userAgent: string;
|
||||
|
||||
|
||||
|
||||
@Column({ type: 'datetime', transformer: new DateTimeTransformer() })
|
||||
@Column({ type: 'timestamp' })
|
||||
startTime: Date;
|
||||
|
||||
@Column({ type: 'real', default: 0 })
|
||||
@Column({ type: 'decimal', default: 0 })
|
||||
bestDifficulty: number
|
||||
|
||||
@Column({ default: 0 })
|
||||
@Column({ default: 0, type: 'decimal' })
|
||||
hashRate: number;
|
||||
|
||||
@OneToMany(
|
||||
() => ClientStatisticsEntity,
|
||||
clientStatisticsEntity => clientStatisticsEntity.client
|
||||
)
|
||||
statistics: ClientStatisticsEntity[]
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Interval } from '@nestjs/schedule';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { BehaviorSubject, firstValueFrom } from 'rxjs';
|
||||
import { ObjectLiteral, Repository } from 'typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ClientEntity } from './client.entity';
|
||||
|
||||
@@ -12,8 +10,6 @@ import { ClientEntity } from './client.entity';
|
||||
export class ClientService {
|
||||
|
||||
|
||||
public insertQueue: { result: BehaviorSubject<ObjectLiteral | null>, partialClient: Partial<ClientEntity> }[] = [];
|
||||
|
||||
|
||||
constructor(
|
||||
@InjectRepository(ClientEntity)
|
||||
@@ -22,31 +18,18 @@ export class ClientService {
|
||||
|
||||
}
|
||||
|
||||
@Interval(1000 * 5)
|
||||
public async insertClients() {
|
||||
const queueCopy = [...this.insertQueue];
|
||||
this.insertQueue = [];
|
||||
|
||||
const results = await this.clientRepository.insert(queueCopy.map(c => c.partialClient));
|
||||
|
||||
queueCopy.forEach((c, index) => {
|
||||
c.result.next(results.generatedMaps[index]);
|
||||
});
|
||||
}
|
||||
|
||||
public async killDeadClients() {
|
||||
var fiveMinutes = new Date(new Date().getTime() - (5 * 60 * 1000)).toISOString();
|
||||
|
||||
return await this.clientRepository
|
||||
.createQueryBuilder()
|
||||
.update(ClientEntity)
|
||||
.set({ deletedAt: () => "DATETIME('now')" })
|
||||
.where("deletedAt IS NULL AND updatedAt < DATETIME(:fiveMinutes)", { fiveMinutes })
|
||||
.set({ deletedAt: () => "NOW()" })
|
||||
.where("deletedAt IS NULL AND updatedAt < NOW() + interval '5 minutes' ")
|
||||
.execute();
|
||||
}
|
||||
|
||||
public async heartbeat(address: string, clientName: string, sessionId: string, hashRate: number, updatedAt: Date) {
|
||||
return await this.clientRepository.update({ address, clientName, sessionId }, { hashRate, deletedAt: null, updatedAt });
|
||||
public async heartbeat(id, hashRate: number, updatedAt: Date) {
|
||||
return await this.clientRepository.update({ id }, { hashRate, deletedAt: null, updatedAt });
|
||||
}
|
||||
|
||||
// public async save(client: Partial<ClientEntity>) {
|
||||
@@ -55,26 +38,18 @@ export class ClientService {
|
||||
|
||||
|
||||
public async insert(partialClient: Partial<ClientEntity>): Promise<ClientEntity> {
|
||||
|
||||
const result = new BehaviorSubject(null);
|
||||
|
||||
this.insertQueue.push({ result, partialClient });
|
||||
|
||||
|
||||
// const insertResult = await this.clientRepository.insert(partialClient);
|
||||
|
||||
const generatedMap = await firstValueFrom(result);
|
||||
const insertResult = await this.clientRepository.insert(partialClient);
|
||||
|
||||
const client = {
|
||||
...partialClient,
|
||||
...generatedMap
|
||||
...insertResult.generatedMaps[0]
|
||||
};
|
||||
|
||||
return client as ClientEntity;
|
||||
}
|
||||
|
||||
public async delete(sessionId: string) {
|
||||
return await this.clientRepository.softDelete({ sessionId });
|
||||
public async delete(id: string) {
|
||||
return await this.clientRepository.softDelete({ id });
|
||||
}
|
||||
|
||||
public async deleteOldClients() {
|
||||
@@ -90,8 +65,8 @@ export class ClientService {
|
||||
|
||||
}
|
||||
|
||||
public async updateBestDifficulty(sessionId: string, bestDifficulty: number) {
|
||||
return await this.clientRepository.update({ sessionId }, { bestDifficulty });
|
||||
public async updateBestDifficulty(id: string, bestDifficulty: number) {
|
||||
return await this.clientRepository.update({ id }, { bestDifficulty });
|
||||
}
|
||||
public async connectedClientCount(): Promise<number> {
|
||||
return await this.clientRepository.count();
|
||||
@@ -129,16 +104,16 @@ export class ClientService {
|
||||
return await this.clientRepository.softDelete({})
|
||||
}
|
||||
|
||||
public async getUserAgents() {
|
||||
const result = await this.clientRepository.createQueryBuilder('client')
|
||||
.select('client.userAgent as userAgent')
|
||||
.addSelect('COUNT(client.userAgent)', 'count')
|
||||
.addSelect('MAX(client.bestDifficulty)', 'bestDifficulty')
|
||||
.addSelect('SUM(client.hashRate)', 'totalHashRate')
|
||||
.groupBy('client.userAgent')
|
||||
.orderBy('count', 'DESC')
|
||||
.getRawMany();
|
||||
return result;
|
||||
}
|
||||
// public async getUserAgents() {
|
||||
// const result = await this.clientRepository.createQueryBuilder('client')
|
||||
// .select('client.userAgent as "userAgent"')
|
||||
// .addSelect('COUNT(client.userAgent)', 'count')
|
||||
// .addSelect('MAX(client.bestDifficulty)', 'bestDifficulty')
|
||||
// .addSelect('SUM(client.hashRate)', 'totalHashRate')
|
||||
// .groupBy('client.userAgent')
|
||||
// .orderBy('"totalHashRate"', 'DESC')
|
||||
// .getRawMany();
|
||||
// return result;
|
||||
// }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class HomeGraphEntity {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ type: 'bigint' })
|
||||
label: number;
|
||||
|
||||
@Column({ type: 'bigint' })
|
||||
data: number;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { HomeGraphEntity } from './home-graph.entity';
|
||||
import { HomeGraphService } from './home-graph.service';
|
||||
|
||||
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([HomeGraphEntity])],
|
||||
providers: [HomeGraphService],
|
||||
exports: [TypeOrmModule, HomeGraphService],
|
||||
})
|
||||
export class HomeGraphModule { }
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { HomeGraphEntity } from './home-graph.entity';
|
||||
|
||||
@Injectable()
|
||||
export class HomeGraphService {
|
||||
constructor(
|
||||
@InjectRepository(HomeGraphEntity) private homeGraphRepository: Repository<HomeGraphEntity>
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
public async getLatestTime(): Promise<Date> {
|
||||
const result = await this.homeGraphRepository.createQueryBuilder('entity')
|
||||
.select('MAX(entity.label)', 'maxNumber')
|
||||
.getRawOne();
|
||||
|
||||
if (result.maxNumber == null) {
|
||||
return new Date(new Date().getTime() - (24 * 60 * 60 * 1000));
|
||||
}
|
||||
|
||||
return new Date(parseInt(result.maxNumber));
|
||||
}
|
||||
|
||||
public async save(graph: { label: number, data: number }[]) {
|
||||
await this.homeGraphRepository.insert(graph);
|
||||
}
|
||||
|
||||
public async getChartDataForSite(limit: number = 144 * 7) {
|
||||
const records = await this.homeGraphRepository
|
||||
.createQueryBuilder('homeGraph')
|
||||
.orderBy('homeGraph.id', 'DESC')
|
||||
.limit(limit)
|
||||
.getRawMany();
|
||||
|
||||
return records.map(res => {
|
||||
return {
|
||||
label: new Date(parseInt(res.homeGraph_label)).toISOString(),
|
||||
data: res.homeGraph_data
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -25,4 +25,19 @@ export class RpcBlockService {
|
||||
public saveBlock(blockHeight: number, data: string) {
|
||||
return this.rpcBlockRepository.update(blockHeight, { data })
|
||||
}
|
||||
|
||||
public async deleteOldBlocks() {
|
||||
const result = await this.rpcBlockRepository.createQueryBuilder('entity')
|
||||
.select('MAX(entity.blockHeight)', 'maxNumber')
|
||||
.getRawOne();
|
||||
|
||||
const newestBlock = result ? result.maxNumber : null;
|
||||
|
||||
await this.rpcBlockRepository.createQueryBuilder()
|
||||
.delete()
|
||||
.where('"blockHeight" < :newestBlock', { newestBlock })
|
||||
.execute();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
import { CreateDateColumn, DeleteDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
|
||||
import { DateTimeTransformer } from './DateTimeTransformer';
|
||||
|
||||
export abstract class TrackedEntity {
|
||||
@DeleteDateColumn({ nullable: true, type: 'datetime', transformer: new DateTimeTransformer() })
|
||||
@DeleteDateColumn({ nullable: true, type: 'timestamp' })
|
||||
public deletedAt?: Date;
|
||||
|
||||
@CreateDateColumn({ type: 'datetime', transformer: new DateTimeTransformer() })
|
||||
@CreateDateColumn({ type: 'timestamp' })
|
||||
public createdAt?: Date
|
||||
|
||||
@UpdateDateColumn({ type: 'datetime', transformer: new DateTimeTransformer() })
|
||||
@UpdateDateColumn({ type: 'timestamp' })
|
||||
public updatedAt?: Date
|
||||
}
|
||||
+16
-8
@@ -3,9 +3,12 @@ import { Controller, Get, Inject } from '@nestjs/common';
|
||||
import { Cache } from 'cache-manager';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
import { UserAgentReportService } from './ORM/_views/user-agent-report/user-agent-report.service';
|
||||
import { AddressSettingsService } from './ORM/address-settings/address-settings.service';
|
||||
import { BlocksService } from './ORM/blocks/blocks.service';
|
||||
import { ClientStatisticsService } from './ORM/client-statistics/client-statistics.service';
|
||||
import { ClientService } from './ORM/client/client.service';
|
||||
import { HomeGraphService } from './ORM/home-graph/home-graph.service';
|
||||
import { BitcoinRpcService } from './services/bitcoin-rpc.service';
|
||||
|
||||
@Controller()
|
||||
@@ -18,7 +21,10 @@ export class AppController {
|
||||
private readonly clientService: ClientService,
|
||||
private readonly clientStatisticsService: ClientStatisticsService,
|
||||
private readonly blocksService: BlocksService,
|
||||
private readonly bitcoinRpcService: BitcoinRpcService
|
||||
private readonly bitcoinRpcService: BitcoinRpcService,
|
||||
private readonly homeGraphService: HomeGraphService,
|
||||
private readonly addressSettingsService: AddressSettingsService,
|
||||
private readonly userAgentReportService: UserAgentReportService
|
||||
) { }
|
||||
|
||||
@Get('info')
|
||||
@@ -34,16 +40,18 @@ export class AppController {
|
||||
|
||||
|
||||
const blockData = await this.blocksService.getFoundBlocks();
|
||||
const userAgents = await this.clientService.getUserAgents();
|
||||
const userAgents = await this.userAgentReportService.getReport();
|
||||
const highScores = await this.addressSettingsService.getHighScores();
|
||||
|
||||
const data = {
|
||||
blockData,
|
||||
userAgents,
|
||||
highScores,
|
||||
uptime: this.uptime
|
||||
};
|
||||
|
||||
//1 min
|
||||
await this.cacheManager.set(CACHE_KEY, data, 1 * 60 * 1000);
|
||||
//5 min
|
||||
await this.cacheManager.set(CACHE_KEY, data, 5 * 60 * 1000);
|
||||
|
||||
return data;
|
||||
|
||||
@@ -60,9 +68,9 @@ export class AppController {
|
||||
}
|
||||
|
||||
|
||||
const userAgents = await this.clientService.getUserAgents();
|
||||
const totalHashRate = userAgents.reduce((acc, userAgent) => acc + parseFloat(userAgent.totalHashRate), 0);
|
||||
const totalMiners = userAgents.reduce((acc, userAgent) => acc + parseInt(userAgent.count), 0);
|
||||
const userAgents = await this.userAgentReportService.getReport();
|
||||
const totalHashRate = userAgents.reduce((acc, userAgent) => acc + userAgent.totalHashRate, 0);
|
||||
const totalMiners = userAgents.reduce((acc, userAgent) => acc + userAgent.count, 0);
|
||||
const blockHeight = (await firstValueFrom(this.bitcoinRpcService.newBlock$)).blocks;
|
||||
const blocksFound = await this.blocksService.getFoundBlocks();
|
||||
|
||||
@@ -97,7 +105,7 @@ export class AppController {
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
const chartData = await this.clientStatisticsService.getChartDataForSite();
|
||||
const chartData = await this.homeGraphService.getChartDataForSite();
|
||||
|
||||
//10 min
|
||||
await this.cacheManager.set(CACHE_KEY, chartData, 10 * 60 * 1000);
|
||||
|
||||
+43
-10
@@ -1,7 +1,7 @@
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { CacheModule } from '@nestjs/cache-manager';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
@@ -9,11 +9,22 @@ import { AppController } from './app.controller';
|
||||
import { AddressController } from './controllers/address/address.controller';
|
||||
import { ClientController } from './controllers/client/client.controller';
|
||||
import { BitcoinAddressValidator } from './models/validators/bitcoin-address.validator';
|
||||
import { UniqueNonceIndex } from './ORM/_migrations/UniqueNonceIndex';
|
||||
import { UserAgentReportModule } from './ORM/_views/user-agent-report/user-agent-report.module';
|
||||
import { UserAgentReportView } from './ORM/_views/user-agent-report/user-agent-report.view';
|
||||
import { AddressSettingsEntity } from './ORM/address-settings/address-settings.entity';
|
||||
import { AddressSettingsModule } from './ORM/address-settings/address-settings.module';
|
||||
import { BlocksEntity } from './ORM/blocks/blocks.entity';
|
||||
import { BlocksModule } from './ORM/blocks/blocks.module';
|
||||
import { ClientStatisticsEntity } from './ORM/client-statistics/client-statistics.entity';
|
||||
import { ClientStatisticsModule } from './ORM/client-statistics/client-statistics.module';
|
||||
import { ClientEntity } from './ORM/client/client.entity';
|
||||
import { ClientModule } from './ORM/client/client.module';
|
||||
import { HomeGraphEntity } from './ORM/home-graph/home-graph.entity';
|
||||
import { HomeGraphModule } from './ORM/home-graph/home-graph.module';
|
||||
import { RpcBlockEntity } from './ORM/rpc-block/rpc-block.entity';
|
||||
import { RpcBlocksModule } from './ORM/rpc-block/rpc-block.module';
|
||||
import { TelegramSubscriptionsEntity } from './ORM/telegram-subscriptions/telegram-subscriptions.entity';
|
||||
import { TelegramSubscriptionsModule } from './ORM/telegram-subscriptions/telegram-subscriptions.module';
|
||||
import { AppService } from './services/app.service';
|
||||
import { BitcoinRpcService } from './services/bitcoin-rpc.service';
|
||||
@@ -33,21 +44,43 @@ const ORMModules = [
|
||||
AddressSettingsModule,
|
||||
TelegramSubscriptionsModule,
|
||||
BlocksModule,
|
||||
RpcBlocksModule
|
||||
RpcBlocksModule,
|
||||
HomeGraphModule,
|
||||
UserAgentReportModule
|
||||
]
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot(),
|
||||
TypeOrmModule.forRoot({
|
||||
type: 'sqlite',
|
||||
database: './DB/public-pool.sqlite',
|
||||
synchronize: true,
|
||||
autoLoadEntities: true,
|
||||
TypeOrmModule.forRootAsync({
|
||||
useFactory: (configService: ConfigService) => {
|
||||
return {
|
||||
type: 'postgres',
|
||||
host: configService.get('DB_HOST'),
|
||||
port: parseInt(configService.get('DB_PORT')),
|
||||
username: configService.get('DB_USERNAME'),
|
||||
password: configService.get('DB_PASSWORD'),
|
||||
database: configService.get('DB_DATABASE'),
|
||||
entities: [
|
||||
ClientEntity,
|
||||
AddressSettingsEntity,
|
||||
BlocksEntity,
|
||||
ClientStatisticsEntity,
|
||||
RpcBlockEntity,
|
||||
TelegramSubscriptionsEntity,
|
||||
HomeGraphEntity,
|
||||
UserAgentReportView
|
||||
],
|
||||
synchronize: configService.get('PRODUCTION') != 'true',
|
||||
logging: false,
|
||||
enableWAL: true,
|
||||
busyTimeout: 30 * 1000,
|
||||
|
||||
poolSize: 30,
|
||||
migrations: [
|
||||
UniqueNonceIndex
|
||||
]
|
||||
}
|
||||
},
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService]
|
||||
}),
|
||||
CacheModule.register(),
|
||||
ScheduleModule.forRoot(),
|
||||
|
||||
@@ -30,7 +30,7 @@ export class ClientController {
|
||||
return {
|
||||
sessionId: worker.sessionId,
|
||||
name: worker.clientName,
|
||||
bestDifficulty: worker.bestDifficulty.toFixed(2),
|
||||
bestDifficulty: parseFloat(worker.bestDifficulty as any).toFixed(2),
|
||||
hashRate: worker.hashRate,
|
||||
startTime: worker.startTime,
|
||||
lastSeen: worker.updatedAt
|
||||
@@ -75,7 +75,7 @@ export class ClientController {
|
||||
if (worker == null) {
|
||||
return new NotFoundException();
|
||||
}
|
||||
const chartData = await this.clientStatisticsService.getChartDataForSession(worker.address, worker.clientName, worker.sessionId);
|
||||
const chartData = await this.clientStatisticsService.getChartDataForSession(worker.id);
|
||||
|
||||
return {
|
||||
sessionId: worker.sessionId,
|
||||
|
||||
@@ -43,7 +43,7 @@ export class StratumV1Client {
|
||||
private usedSuggestedDifficulty = false;
|
||||
private sessionDifficulty: number = 16384;
|
||||
|
||||
private entity: ClientEntity;
|
||||
private clientEntity: ClientEntity;
|
||||
private creatingEntity: Promise<void>;
|
||||
|
||||
public extraNonceAndSessionId: string;
|
||||
@@ -83,7 +83,9 @@ export class StratumV1Client {
|
||||
|
||||
public async destroy() {
|
||||
|
||||
await this.clientService.delete(this.extraNonceAndSessionId);
|
||||
if (this.clientEntity?.id) {
|
||||
await this.clientService.delete(this.clientEntity.id);
|
||||
}
|
||||
|
||||
if (this.stratumSubscription != null) {
|
||||
this.stratumSubscription.unsubscribe();
|
||||
@@ -335,7 +337,7 @@ export class StratumV1Client {
|
||||
&& this.clientAuthorization != null
|
||||
&& this.stratumInitialized == false) {
|
||||
|
||||
await this.initStratum();
|
||||
this.initStratum();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -373,11 +375,11 @@ export class StratumV1Client {
|
||||
}, 60 * 1000)
|
||||
);
|
||||
|
||||
this.backgroundWork.push(
|
||||
setInterval(async () => {
|
||||
await this.statistics.saveShares(this.entity);
|
||||
}, 60 * 1000)
|
||||
);
|
||||
// this.backgroundWork.push(
|
||||
// setInterval(async () => {
|
||||
// await this.statistics.saveShares(this.clientEntity);
|
||||
// }, 60 * 1000)
|
||||
// );
|
||||
}
|
||||
|
||||
private async sendNewMiningJob(jobTemplate: IJobTemplate) {
|
||||
@@ -386,8 +388,8 @@ export class StratumV1Client {
|
||||
const devFeeAddress = this.configService.get('DEV_FEE_ADDRESS');
|
||||
//50Th/s
|
||||
this.noFee = false;
|
||||
if (this.entity) {
|
||||
this.hashRate = await this.clientStatisticsService.getHashRateForSession(this.clientAuthorization.address, this.clientAuthorization.worker, this.extraNonceAndSessionId);
|
||||
if (this.clientEntity) {
|
||||
this.hashRate = await this.clientStatisticsService.getHashRateForSession(this.clientEntity.id);
|
||||
this.noFee = this.hashRate != 0 && this.hashRate < 50000000000000;
|
||||
}
|
||||
if (this.noFee || devFeeAddress == null || devFeeAddress.length < 1) {
|
||||
@@ -438,11 +440,11 @@ export class StratumV1Client {
|
||||
|
||||
private async handleMiningSubmission(submission: MiningSubmitMessage) {
|
||||
|
||||
if (this.entity == null) {
|
||||
if (this.clientEntity == null) {
|
||||
if (this.creatingEntity == null) {
|
||||
this.creatingEntity = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
this.entity = await this.clientService.insert({
|
||||
this.clientEntity = await this.clientService.insert({
|
||||
sessionId: this.extraNonceAndSessionId,
|
||||
address: this.clientAuthorization.address,
|
||||
clientName: this.clientAuthorization.worker,
|
||||
@@ -515,12 +517,12 @@ export class StratumV1Client {
|
||||
}
|
||||
}
|
||||
try {
|
||||
await this.statistics.addShares(this.sessionDifficulty);
|
||||
await this.statistics.addShares(this.clientEntity, this.sessionDifficulty);
|
||||
const now = new Date();
|
||||
// only update every minute
|
||||
if (this.entity.updatedAt == null || now.getTime() - this.entity.updatedAt.getTime() > 1000 * 60) {
|
||||
await this.clientService.heartbeat(this.entity.address, this.entity.clientName, this.entity.sessionId, this.hashRate, now);
|
||||
this.entity.updatedAt = now;
|
||||
if (this.clientEntity.updatedAt == null || now.getTime() - this.clientEntity.updatedAt.getTime() > 1000 * 60) {
|
||||
await this.clientService.heartbeat(this.clientEntity.id, this.hashRate, now);
|
||||
this.clientEntity.updatedAt = now;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
@@ -537,11 +539,11 @@ export class StratumV1Client {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (submissionDifficulty > this.entity.bestDifficulty) {
|
||||
await this.clientService.updateBestDifficulty(this.extraNonceAndSessionId, submissionDifficulty);
|
||||
this.entity.bestDifficulty = submissionDifficulty;
|
||||
if (submissionDifficulty > this.clientEntity.bestDifficulty) {
|
||||
await this.clientService.updateBestDifficulty(this.clientEntity.id, submissionDifficulty);
|
||||
this.clientEntity.bestDifficulty = submissionDifficulty;
|
||||
if (submissionDifficulty > (await this.addressSettingsService.getSettings(this.clientAuthorization.address, true)).bestDifficulty) {
|
||||
await this.addressSettingsService.updateBestDifficulty(this.clientAuthorization.address, submissionDifficulty);
|
||||
await this.addressSettingsService.updateBestDifficulty(this.clientAuthorization.address, submissionDifficulty, this.clientEntity.userAgent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -631,7 +633,7 @@ export class StratumV1Client {
|
||||
|
||||
return true;
|
||||
} else {
|
||||
console.error(`Error: Cannot write to closed or ended socket. ${this.extraNonceAndSessionId} ${message}`);
|
||||
//console.error(`Error: Cannot write to closed or ended socket. ${this.extraNonceAndSessionId} ${message}`);
|
||||
this.destroy();
|
||||
if (!this.socket.destroyed) {
|
||||
this.socket.destroy();
|
||||
@@ -645,7 +647,7 @@ export class StratumV1Client {
|
||||
} else if (!this.socket.destroyed) {
|
||||
this.socket.destroy();
|
||||
}
|
||||
console.error(`Error occurred while writing to socket: ${this.extraNonceAndSessionId}`, error);
|
||||
//console.error(`Error occurred while writing to socket: ${this.extraNonceAndSessionId}`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,14 @@ const TARGET_SUBMISSION_PER_SECOND = 10;
|
||||
const MIN_DIFF = 0.00001;
|
||||
export class StratumV1ClientStatistics {
|
||||
|
||||
private shareBacklog: number = 0;
|
||||
private shares: number = 0;
|
||||
private acceptedCount: number = 0;
|
||||
|
||||
private submissionCacheStart: Date;
|
||||
private submissionCache = [];
|
||||
private submissionCache: { time: Date, difficulty: number }[] = [];
|
||||
|
||||
private currentTimeSlot: number = null;
|
||||
private lastSave: number = null;
|
||||
|
||||
constructor(
|
||||
private readonly clientStatisticsService: ClientStatisticsService
|
||||
@@ -20,40 +24,60 @@ export class StratumV1ClientStatistics {
|
||||
|
||||
public async saveShares(client: ClientEntity) {
|
||||
|
||||
if (client == null || client.address == null || client.clientName == null || client.sessionId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 10 min
|
||||
var coeff = 1000 * 60 * 10;
|
||||
var date = new Date();
|
||||
var rounded = new Date(Math.floor(date.getTime() / coeff) * coeff);
|
||||
// if (client == null || client.address == null || client.clientName == null || client.sessionId == null) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
await this.clientStatisticsService.save({
|
||||
time: rounded.getTime(),
|
||||
shares: this.shareBacklog,
|
||||
time: this.currentTimeSlot,
|
||||
clientId: client.id,
|
||||
shares: this.shares,
|
||||
acceptedCount: this.acceptedCount,
|
||||
address: client.address,
|
||||
clientName: client.clientName,
|
||||
sessionId: client.sessionId
|
||||
});
|
||||
|
||||
this.shareBacklog = 0;
|
||||
|
||||
|
||||
}
|
||||
|
||||
// We don't want to save them here because it can be DB intensive, stead do it every once in
|
||||
// We don't want to save them here because it can be DB intensive, instead do it every once in
|
||||
// awhile with saveShares()
|
||||
public async addShares(targetDifficulty: number) {
|
||||
public async addShares(client: ClientEntity, targetDifficulty: number) {
|
||||
|
||||
// 10 min
|
||||
var coeff = 1000 * 60 * 10;
|
||||
var date = new Date();
|
||||
var timeSlot = new Date(Math.floor(date.getTime() / coeff) * coeff).getTime();
|
||||
|
||||
if (this.submissionCache.length > CACHE_SIZE) {
|
||||
this.submissionCache.shift();
|
||||
}
|
||||
|
||||
this.submissionCache.push({
|
||||
time: new Date(),
|
||||
time: date,
|
||||
difficulty: targetDifficulty,
|
||||
});
|
||||
|
||||
this.shareBacklog += targetDifficulty;
|
||||
|
||||
|
||||
if (this.currentTimeSlot == null) {
|
||||
this.currentTimeSlot = timeSlot;
|
||||
}
|
||||
|
||||
if (this.currentTimeSlot != timeSlot) {
|
||||
await this.saveShares(client);
|
||||
this.shares = 0;
|
||||
this.acceptedCount = 0;
|
||||
this.currentTimeSlot = timeSlot;
|
||||
} else if ((date.getTime() - 60 * 1000) > this.lastSave) {
|
||||
await this.saveShares(client);
|
||||
this.lastSave = new Date().getTime();
|
||||
}
|
||||
|
||||
|
||||
this.shares += targetDifficulty;
|
||||
this.acceptedCount++;
|
||||
|
||||
}
|
||||
|
||||
|
||||
+71
-24
@@ -1,9 +1,11 @@
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { Interval } from '@nestjs/schedule';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { UserAgentReportService } from '../ORM/_views/user-agent-report/user-agent-report.service';
|
||||
import { ClientStatisticsService } from '../ORM/client-statistics/client-statistics.service';
|
||||
import { ClientService } from '../ORM/client/client.service';
|
||||
import { HomeGraphService } from '../ORM/home-graph/home-graph.service';
|
||||
import { RpcBlockService } from '../ORM/rpc-block/rpc-block.service';
|
||||
|
||||
@Injectable()
|
||||
export class AppService implements OnModuleInit {
|
||||
@@ -11,42 +13,87 @@ export class AppService implements OnModuleInit {
|
||||
constructor(
|
||||
private readonly clientStatisticsService: ClientStatisticsService,
|
||||
private readonly clientService: ClientService,
|
||||
private readonly dataSource: DataSource
|
||||
private readonly rpcBlockService: RpcBlockService,
|
||||
private readonly homeGraphService: HomeGraphService,
|
||||
private readonly dataSource: DataSource,
|
||||
private readonly userAgentReportService: UserAgentReportService
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
// if (process.env.NODE_APP_INSTANCE == '0') {
|
||||
// await this.dataSource.query(`VACUUM;`);
|
||||
// }
|
||||
if (process.env.ENABLE_SOLO == 'true' && (process.env.NODE_APP_INSTANCE == null || process.env.NODE_APP_INSTANCE == '0')) {
|
||||
|
||||
setInterval(async () => {
|
||||
await this.deleteOldStatistics();
|
||||
}, 1000 * 60 * 60);
|
||||
|
||||
setInterval(async () => {
|
||||
console.log('Killing dead clients');
|
||||
await this.clientService.killDeadClients();
|
||||
}, 1000 * 60 * 5);
|
||||
|
||||
setInterval(async () => {
|
||||
console.log('Deleting Old Blocks');
|
||||
await this.rpcBlockService.deleteOldBlocks();
|
||||
}, 1000 * 60 * 60 * 24);
|
||||
|
||||
setInterval(async () => {
|
||||
await this.updateChart();
|
||||
}, 1000 * 60 * 10);
|
||||
|
||||
|
||||
setInterval(async () => {
|
||||
console.log('Refreshing user agent report view')
|
||||
await this.userAgentReportService.refreshReport();
|
||||
console.log('Finished Refreshing user agent report view')
|
||||
}, 1000 * 60 * 5);
|
||||
|
||||
}
|
||||
|
||||
//https://phiresky.github.io/blog/2020/sqlite-performance-tuning/
|
||||
// //500 MB DB cache
|
||||
// await this.dataSource.query(`PRAGMA cache_size = -500000;`);
|
||||
//Normal is still completely corruption safe in WAL mode, and means only WAL checkpoints have to wait for FSYNC.
|
||||
await this.dataSource.query(`PRAGMA synchronous = off;`);
|
||||
// //6Gb
|
||||
// await this.dataSource.query(`PRAGMA mmap_size = 6000000000;`);
|
||||
}
|
||||
|
||||
@Interval(1000 * 60 * 60)
|
||||
private async deleteOldStatistics() {
|
||||
console.log('Deleting statistics');
|
||||
if (process.env.ENABLE_SOLO == 'true' && (process.env.NODE_APP_INSTANCE == null || process.env.NODE_APP_INSTANCE == '0')) {
|
||||
|
||||
const deletedStatistics = await this.clientStatisticsService.deleteOldStatistics();
|
||||
console.log(`Deleted ${deletedStatistics.affected} old statistics`);
|
||||
const deletedClients = await this.clientService.deleteOldClients();
|
||||
console.log(`Deleted ${deletedClients.affected} old clients`);
|
||||
}
|
||||
}
|
||||
|
||||
@Interval(1000 * 60 * 5)
|
||||
private async killDeadClients() {
|
||||
console.log('Killing dead clients');
|
||||
if (process.env.ENABLE_SOLO == 'true' && (process.env.NODE_APP_INSTANCE == null || process.env.NODE_APP_INSTANCE == '0')) {
|
||||
await this.clientService.killDeadClients();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private async updateChart() {
|
||||
console.log('Updating Chart');
|
||||
|
||||
const latestGraphUpdate = await this.homeGraphService.getLatestTime();
|
||||
|
||||
|
||||
const data = await this.dataSource.query(`
|
||||
SELECT
|
||||
time AS label,
|
||||
ROUND(((SUM(shares) * 4294967296) / 600)) AS data
|
||||
FROM
|
||||
client_statistics_entity AS entry
|
||||
WHERE
|
||||
entry.time > ${latestGraphUpdate.getTime()}
|
||||
GROUP BY
|
||||
time
|
||||
ORDER BY
|
||||
time
|
||||
LIMIT 144;
|
||||
`)
|
||||
|
||||
console.log(`Fetched ${data.length} rows`);
|
||||
|
||||
if (data.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = data.slice(0, data.length - 1);
|
||||
|
||||
this.homeGraphService.save(result);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { RPCClient } from 'rpc-bitcoin';
|
||||
import { BehaviorSubject, filter, shareReplay } from 'rxjs';
|
||||
import { RpcBlockService } from 'src/ORM/rpc-block/rpc-block.service';
|
||||
import * as zmq from 'zeromq/v5-compat';
|
||||
import * as zmq from 'zeromq';
|
||||
|
||||
import { IBlockTemplate } from '../models/bitcoin-rpc/IBlockTemplate';
|
||||
import { IMiningInfo } from '../models/bitcoin-rpc/IMiningInfo';
|
||||
|
||||
@Injectable()
|
||||
export class BitcoinRpcService {
|
||||
export class BitcoinRpcService implements OnModuleInit {
|
||||
|
||||
private blockHeight = 0;
|
||||
private client: RPCClient;
|
||||
@@ -21,6 +21,10 @@ export class BitcoinRpcService {
|
||||
private rpcBlockService: RpcBlockService
|
||||
) {
|
||||
|
||||
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
const url = this.configService.get('BITCOIN_RPC_URL');
|
||||
const user = this.configService.get('BITCOIN_RPC_USER');
|
||||
const pass = this.configService.get('BITCOIN_RPC_PASSWORD');
|
||||
@@ -36,19 +40,36 @@ export class BitcoinRpcService {
|
||||
});
|
||||
|
||||
if (this.configService.get('BITCOIN_ZMQ_HOST')) {
|
||||
const sock = zmq.socket("sub");
|
||||
sock.connect(this.configService.get('BITCOIN_ZMQ_HOST'));
|
||||
sock.subscribe("rawblock");
|
||||
sock.on("message", async (topic: Buffer, message: Buffer) => {
|
||||
console.log("new block zmq");
|
||||
await this.pollMiningInfo();
|
||||
console.log('Using ZMQ');
|
||||
const sock = new zmq.Subscriber;
|
||||
|
||||
|
||||
sock.connectTimeout = 1000;
|
||||
sock.events.on('connect', () => {
|
||||
console.log('ZMQ Connected');
|
||||
});
|
||||
this.pollMiningInfo().then(() => { });
|
||||
sock.events.on('connect:retry', () => {
|
||||
console.log('ZMQ Unable to connect, Retrying');
|
||||
});
|
||||
|
||||
sock.connect(this.configService.get('BITCOIN_ZMQ_HOST'));
|
||||
sock.subscribe('rawblock');
|
||||
// Don't await this, otherwise it will block the rest of the program
|
||||
this.listenForNewBlocks(sock);
|
||||
await this.pollMiningInfo();
|
||||
|
||||
} else {
|
||||
setInterval(this.pollMiningInfo.bind(this), 500);
|
||||
}
|
||||
}
|
||||
|
||||
private async listenForNewBlocks(sock: zmq.Subscriber) {
|
||||
for await (const [topic, msg] of sock) {
|
||||
console.log("New Block");
|
||||
await this.pollMiningInfo();
|
||||
}
|
||||
}
|
||||
|
||||
public async pollMiningInfo() {
|
||||
const miningInfo = await this.getMiningInfo();
|
||||
if (miningInfo != null && miningInfo.blocks > this.blockHeight) {
|
||||
|
||||
Reference in New Issue
Block a user