34 Commits
Author SHA1 Message Date
Ben Wilson 87508bfb69 user agent report 2024-02-16 23:49:47 -05:00
Ben Wilson 39e537a00c bug 2024-02-16 17:18:21 -05:00
Ben Wilson 9ecfc68155 bug 2024-02-16 17:17:19 -05:00
Ben Wilson 3e2de62259 db refactor 2024-02-16 17:12:36 -05:00
Ben Wilson 8e977c21a1 Merge remote-tracking branch 'origin/master' into postgresql 2024-02-14 08:38:05 -05:00
Ben Wilson 5fd9f98807 index, dead code 2024-01-22 17:32:34 -05:00
Ben Wilson 6bc42defef separate home graph into it's own table 2024-01-16 19:37:29 -05:00
Ben Wilson bbc14e2272 bug 2024-01-15 20:04:26 -05:00
Ben Wilson fc767fa770 reduce heartbeat updates 2024-01-14 14:10:47 -05:00
Ben Wilson 40e32d6e1a stratum jobs array to dictionary 2023-12-15 00:56:22 -05:00
Ben Wilson df6ec07c58 batch save share statistics 2023-12-15 00:25:01 -05:00
Ben Wilson df245f396e parameterized typeorm 2023-12-14 23:54:45 -05:00
Ben Wilson 42fc863c7c column names 2023-12-14 23:47:51 -05:00
Ben Wilson e46bb24155 raw query 2023-12-14 23:44:41 -05:00
Ben Wilson cb4a173733 add back index with time 2023-12-14 22:45:12 -05:00
Ben Wilson 83c769849d removed hash from client stats table, connection pool size 2023-12-14 22:30:55 -05:00
Ben Wilson ee5fea1df5 wrap save in a transaction 2023-12-10 17:23:08 -05:00
Ben Wilson 06acad6770 example 2023-12-08 00:12:25 -05:00
Ben Wilson f7b22bb310 delete old stats sooner 2023-12-07 09:57:20 -05:00
Ben Wilson 6cf6a007d9 remove read UNCOMMITTED 2023-12-07 09:55:36 -05:00
Ben Wilson 5b71632f8a postgres doesn't support read UNCOMMITTED 2023-12-07 09:55:07 -05:00
Ben Wilson c5ff8521ac Merge branch 'postgresql' of https://github.com/benjamin-wilson/public-pool into postgresql 2023-12-07 09:53:40 -05:00
Ben Wilson 53ee510a1e fix chart, caching 2023-12-07 09:53:16 -05:00
Ben Wilson 7be554a358 fix chart, caching 2023-12-07 09:40:32 -05:00
Ben Wilson eb5d434157 few optimizations 2023-12-07 09:07:37 -05:00
Ben Wilson 893c7c2003 revert 2023-12-07 08:33:43 -05:00
Ben Wilson 4736eadf7b row names 2023-12-07 08:19:51 -05:00
Ben Wilson 1089e6210c deadlock? 2023-12-07 08:16:48 -05:00
Ben Wilson 017978d250 userAgent 2023-12-04 08:52:49 -05:00
Ben Wilson 5a17528b8f bug 2023-12-04 08:33:47 -05:00
Ben Wilson 0b147ea5db fix numbers returned from query as string 2023-12-04 08:26:47 -05:00
Ben Wilson bffcf6c74c config 2023-12-04 01:07:45 -05:00
Ben Wilson 96a38f9d9b bug 2023-12-04 01:06:53 -05:00
Ben Wilson 39c4c033e2 postgres init 2023-12-04 00:34:33 -05:00
25 changed files with 778 additions and 265 deletions
+9
View File
@@ -38,3 +38,12 @@ ENABLE_PROXY=true
BRAIINS_ACCESS_TOKEN= BRAIINS_ACCESS_TOKEN=
PROXY_PORT=3333 PROXY_PORT=3333
#postgresql
DB_HOST=
DB_PORT=
DB_USERNAME=
DB_PASSWORD=
DB_DATABASE=
PRODUCTION=false
+240
View File
@@ -29,6 +29,7 @@
"discord.js": "^14.11.0", "discord.js": "^14.11.0",
"merkle-lib": "^2.0.10", "merkle-lib": "^2.0.10",
"node-telegram-bot-api": "^0.61.0", "node-telegram-bot-api": "^0.61.0",
"pg": "^8.11.3",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rpc-bitcoin": "^2.0.0", "rpc-bitcoin": "^2.0.0",
"rxjs": "^7.2.0", "rxjs": "^7.2.0",
@@ -3740,6 +3741,14 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true "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": { "node_modules/buffer-xor": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
@@ -8656,6 +8665,11 @@
"node": ">=6" "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": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "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", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" "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": { "node_modules/picocolors": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@@ -8934,6 +9031,41 @@
"node": ">=4" "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": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "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": { "node_modules/y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
@@ -14264,6 +14404,11 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true "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": { "buffer-xor": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
@@ -17987,6 +18132,11 @@
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true "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": { "parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "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", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" "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": { "picocolors": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@@ -18198,6 +18410,29 @@
"integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
"dev": true "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": { "prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "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==", "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
"requires": {} "requires": {}
}, },
"xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
},
"y18n": { "y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+1
View File
@@ -40,6 +40,7 @@
"discord.js": "^14.11.0", "discord.js": "^14.11.0",
"merkle-lib": "^2.0.10", "merkle-lib": "^2.0.10",
"node-telegram-bot-api": "^0.61.0", "node-telegram-bot-api": "^0.61.0",
"pg": "^8.11.3",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rpc-bitcoin": "^2.0.0", "rpc-bitcoin": "^2.0.0",
"rxjs": "^7.2.0", "rxjs": "^7.2.0",
+15
View File
@@ -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 }) @Column({ default: 0 })
shares: number; shares: number;
@Column({ type: 'real', default: 0 }) @Column({ type: 'decimal', default: 0 })
bestDifficulty: number; bestDifficulty: number;
@Column({ nullable: true })
bestDifficultyUserAgent: string;
@Column({ nullable: true }) @Column({ nullable: true })
miscCoinbaseScriptData: string; miscCoinbaseScriptData: string;
@@ -17,13 +17,18 @@ export class AddressSettingsService {
public async getSettings(address: string, createIfNotFound: boolean) { public async getSettings(address: string, createIfNotFound: boolean) {
const settings = await this.addressSettingsRepository.findOne({ where: { address } }); const settings = await this.addressSettingsRepository.findOne({ where: { address } });
if (createIfNotFound == true && settings == null) { if (createIfNotFound == true && settings == null) {
return await this.createNew(address); // 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; return settings;
} }
public async updateBestDifficulty(address: string, bestDifficulty: number) { public async updateBestDifficulty(address: string, bestDifficulty: number, bestDifficultyUserAgent: string) {
return await this.addressSettingsRepository.update({ address }, { bestDifficulty }); return await this.addressSettingsRepository.update({ address }, { bestDifficulty, bestDifficultyUserAgent });
} }
public async createNew(address: string) { public async createNew(address: string) {
@@ -46,4 +51,12 @@ export class AddressSettingsService {
bestDifficulty: 0 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'; import { TrackedEntity } from '../utils/TrackedEntity.entity';
@Entity() @Entity()
//Index for getHashRateForSession
@Index(["address", "clientName", "sessionId"])
//Index for statistics save //Index for statistics save
@Index(["address", "clientName", "sessionId", "time"]) @Index(["clientId", "time"])
export class ClientStatisticsEntity extends TrackedEntity { export class ClientStatisticsEntity extends TrackedEntity {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
@@ -22,14 +21,27 @@ export class ClientStatisticsEntity extends TrackedEntity {
sessionId: string; sessionId: string;
@Index() @Index()
@Column({ type: 'integer' }) @Column({ type: 'bigint' })
time: number; time: number;
@Column({ type: 'real' }) @Column({ type: 'decimal' })
shares: number; shares: number;
@Column({ default: 0, type: 'integer' }) @Column({ default: 0, type: 'bigint' })
acceptedCount: number; 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 { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { DataSource, Repository } from 'typeorm';
import { ClientStatisticsEntity } from './client-statistics.entity'; import { ClientStatisticsEntity } from './client-statistics.entity';
@@ -10,7 +10,8 @@ export class ClientStatisticsService {
constructor( constructor(
@InjectDataSource()
private dataSource: DataSource,
@InjectRepository(ClientStatisticsEntity) @InjectRepository(ClientStatisticsEntity)
private clientStatisticsRepository: Repository<ClientStatisticsEntity>, private clientStatisticsRepository: Repository<ClientStatisticsEntity>,
) { ) {
@@ -18,28 +19,19 @@ export class ClientStatisticsService {
} }
public async save(clientStatistic: Partial<ClientStatisticsEntity>) { public async save(clientStatistic: Partial<ClientStatisticsEntity>) {
// Attempt to update the existing record // // Attempt to update the existing record
const updateResult = await this.clientStatisticsRepository const result = await this.clientStatisticsRepository.update({ clientId: clientStatistic.clientId, time: clientStatistic.time },
.createQueryBuilder() {
.update(ClientStatisticsEntity) shares: clientStatistic.shares,
.set({ acceptedCount: clientStatistic.acceptedCount
shares: () => `"shares" + :sharesIncrement`,
acceptedCount: () => `"acceptedCount" + 1`
}) })
.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 // Check if the update affected any rows
if (updateResult.affected === 0) { if (result.affected === 0) {
// If no rows were updated, insert a new record // If no rows were updated, insert a new record
await this.clientStatisticsRepository.insert(clientStatistic); await this.clientStatisticsRepository.insert(clientStatistic);
} }
} }
public async deleteOldStatistics() { public async deleteOldStatistics() {
@@ -53,57 +45,7 @@ export class ClientStatisticsService {
.execute(); .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) { public async getChartDataForAddress(address: string) {
@@ -111,12 +53,12 @@ export class ClientStatisticsService {
const query = ` const query = `
SELECT SELECT
time label, time AS label,
(SUM(shares) * 4294967296) / 600 AS data (SUM(shares) * 4294967296) / 600 AS data
FROM FROM
client_statistics_entity AS entry client_statistics_entity AS entry
WHERE WHERE
entry.address = ? AND entry.time > ${yesterday.getTime()} entry.address = $1 AND entry.time > $2
GROUP BY GROUP BY
time time
ORDER BY 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 => { return result.map(res => {
res.label = new Date(res.label).toISOString(); res.label = new Date(parseInt(res.label)).toISOString();
return res; return res;
}).slice(0, result.length - 1); }).slice(0, result.length - 1);
@@ -146,7 +88,7 @@ export class ClientStatisticsService {
FROM FROM
client_statistics_entity AS entry client_statistics_entity AS entry
WHERE 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]); const result = await this.clientStatisticsRepository.query(query, [address, clientName]);
@@ -163,12 +105,12 @@ export class ClientStatisticsService {
const query = ` const query = `
SELECT SELECT
time label, time AS label,
(SUM(shares) * 4294967296) / 600 AS data (SUM(shares) * 4294967296) / 600 AS data
FROM FROM
client_statistics_entity AS entry client_statistics_entity AS entry
WHERE 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 GROUP BY
time time
ORDER BY ORDER BY
@@ -179,7 +121,7 @@ export class ClientStatisticsService {
const result = await this.clientStatisticsRepository.query(query, [address, clientName]); const result = await this.clientStatisticsRepository.query(query, [address, clientName]);
return result.map(res => { return result.map(res => {
res.label = new Date(res.label).toISOString(); res.label = new Date(parseInt(res.label)).toISOString();
return res; return res;
}).slice(0, result.length - 1); }).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 = ` const query = `
SELECT SELECT
createdAt, "createdAt",
updatedAt, "updatedAt",
shares shares
FROM FROM
client_statistics_entity AS entry client_statistics_entity AS entry
WHERE WHERE
entry.address = ? AND entry.clientName = ? AND entry.sessionId = ? entry."clientId" = $1
ORDER BY time DESC ORDER BY time DESC
LIMIT 2; LIMIT 2;
`; `;
const result = await this.clientStatisticsRepository.query(query, [address, clientName, sessionId]); const result = await this.clientStatisticsRepository.query(query, [clientId]);
if (result.length < 1) { if (result.length < 1) {
return 0; return 0;
@@ -215,29 +157,29 @@ export class ClientStatisticsService {
if (time < 1) { if (time < 1) {
return 0; return 0;
} }
return (latestStat.shares * 4294967296) / (time / 1000); return (parseFloat(latestStat.shares) * 4294967296) / (time / 1000);
} else { } else {
const secondLatestStat = result[1]; const secondLatestStat = result[1];
const time = new Date(latestStat.updatedAt).getTime() - new Date(secondLatestStat.createdAt).getTime(); const time = new Date(latestStat.updatedAt).getTime() - new Date(secondLatestStat.createdAt).getTime();
if (time < 1) { if (time < 1) {
return 0; 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)); var yesterday = new Date(new Date().getTime() - (24 * 60 * 60 * 1000));
const query = ` const query = `
SELECT SELECT
time label, time AS label,
(SUM(shares) * 4294967296) / 600 AS data (SUM(shares) * 4294967296) / 600 AS data
FROM FROM
client_statistics_entity AS entry client_statistics_entity AS entry
WHERE WHERE
entry.address = ? AND entry.clientName = ? AND entry.sessionId = ? AND entry.time > ${yesterday.getTime()} entry."clientId" = $1 AND entry.time > ${yesterday.getTime()}
GROUP BY GROUP BY
time time
ORDER BY ORDER BY
@@ -245,10 +187,10 @@ export class ClientStatisticsService {
LIMIT 144; LIMIT 144;
`; `;
const result = await this.clientStatisticsRepository.query(query, [address, clientName, sessionId]); const result = await this.clientStatisticsRepository.query(query, [clientId]);
return result.map(res => { return result.map(res => {
res.label = new Date(res.label).toISOString(); res.label = new Date(parseInt(res.label)).toISOString();
return res; return res;
}).slice(0, result.length - 1); }).slice(0, result.length - 1);
+19 -15
View File
@@ -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'; 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 @Entity()
// or composite (multi-column) PRIMARY KEYs and that do not store large strings or BLOBs. @Index("IDX_unique_nonce", { synchronize: false })
//WITHOUT ROWID tables work best when individual rows are not too large.
@Entity({ withoutRowid: true })
@Index(['address', 'clientName', 'sessionId'], { unique: true })
export class ClientEntity extends TrackedEntity { export class ClientEntity extends TrackedEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@PrimaryColumn({ length: 62, type: 'varchar' }) @Index()
@Column({ length: 62, type: 'varchar' })
address: string; address: string;
@PrimaryColumn({ length: 64, type: 'varchar' }) @Column({ length: 64, type: 'varchar' })
clientName: string; clientName: string;
@PrimaryColumn({ length: 8, type: 'varchar' }) @Column({ length: 8, type: 'varchar', })
sessionId: string; sessionId: string;
@@ -27,15 +26,20 @@ export class ClientEntity extends TrackedEntity {
userAgent: string; userAgent: string;
@Column({ type: 'timestamp' })
@Column({ type: 'datetime', transformer: new DateTimeTransformer() })
startTime: Date; startTime: Date;
@Column({ type: 'real', default: 0 }) @Column({ type: 'decimal', default: 0 })
bestDifficulty: number bestDifficulty: number
@Column({ default: 0 }) @Column({ default: 0, type: 'decimal' })
hashRate: number; hashRate: number;
@OneToMany(
() => ClientStatisticsEntity,
clientStatisticsEntity => clientStatisticsEntity.client
)
statistics: ClientStatisticsEntity[]
} }
+22 -47
View File
@@ -1,8 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Interval } from '@nestjs/schedule';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { BehaviorSubject, firstValueFrom } from 'rxjs'; import { Repository } from 'typeorm';
import { ObjectLiteral, Repository } from 'typeorm';
import { ClientEntity } from './client.entity'; import { ClientEntity } from './client.entity';
@@ -12,8 +10,6 @@ import { ClientEntity } from './client.entity';
export class ClientService { export class ClientService {
public insertQueue: { result: BehaviorSubject<ObjectLiteral | null>, partialClient: Partial<ClientEntity> }[] = [];
constructor( constructor(
@InjectRepository(ClientEntity) @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() { public async killDeadClients() {
var fiveMinutes = new Date(new Date().getTime() - (5 * 60 * 1000)).toISOString();
return await this.clientRepository return await this.clientRepository
.createQueryBuilder() .createQueryBuilder()
.update(ClientEntity) .update(ClientEntity)
.set({ deletedAt: () => "DATETIME('now')" }) .set({ deletedAt: () => "NOW()" })
.where("deletedAt IS NULL AND updatedAt < DATETIME(:fiveMinutes)", { fiveMinutes }) .where("deletedAt IS NULL AND updatedAt < NOW() + interval '5 minutes' ")
.execute(); .execute();
} }
public async heartbeat(address: string, clientName: string, sessionId: string, hashRate: number, updatedAt: Date) { public async heartbeat(id, hashRate: number, updatedAt: Date) {
return await this.clientRepository.update({ address, clientName, sessionId }, { hashRate, deletedAt: null, updatedAt }); return await this.clientRepository.update({ id }, { hashRate, deletedAt: null, updatedAt });
} }
// public async save(client: Partial<ClientEntity>) { // public async save(client: Partial<ClientEntity>) {
@@ -55,26 +38,18 @@ export class ClientService {
public async insert(partialClient: Partial<ClientEntity>): Promise<ClientEntity> { public async insert(partialClient: Partial<ClientEntity>): Promise<ClientEntity> {
const insertResult = await this.clientRepository.insert(partialClient);
const result = new BehaviorSubject(null);
this.insertQueue.push({ result, partialClient });
// const insertResult = await this.clientRepository.insert(partialClient);
const generatedMap = await firstValueFrom(result);
const client = { const client = {
...partialClient, ...partialClient,
...generatedMap ...insertResult.generatedMaps[0]
}; };
return client as ClientEntity; return client as ClientEntity;
} }
public async delete(sessionId: string) { public async delete(id: string) {
return await this.clientRepository.softDelete({ sessionId }); return await this.clientRepository.softDelete({ id });
} }
public async deleteOldClients() { public async deleteOldClients() {
@@ -90,8 +65,8 @@ export class ClientService {
} }
public async updateBestDifficulty(sessionId: string, bestDifficulty: number) { public async updateBestDifficulty(id: string, bestDifficulty: number) {
return await this.clientRepository.update({ sessionId }, { bestDifficulty }); return await this.clientRepository.update({ id }, { bestDifficulty });
} }
public async connectedClientCount(): Promise<number> { public async connectedClientCount(): Promise<number> {
return await this.clientRepository.count(); return await this.clientRepository.count();
@@ -129,16 +104,16 @@ export class ClientService {
return await this.clientRepository.softDelete({}) return await this.clientRepository.softDelete({})
} }
public async getUserAgents() { // public async getUserAgents() {
const result = await this.clientRepository.createQueryBuilder('client') // const result = await this.clientRepository.createQueryBuilder('client')
.select('client.userAgent as userAgent') // .select('client.userAgent as "userAgent"')
.addSelect('COUNT(client.userAgent)', 'count') // .addSelect('COUNT(client.userAgent)', 'count')
.addSelect('MAX(client.bestDifficulty)', 'bestDifficulty') // .addSelect('MAX(client.bestDifficulty)', 'bestDifficulty')
.addSelect('SUM(client.hashRate)', 'totalHashRate') // .addSelect('SUM(client.hashRate)', 'totalHashRate')
.groupBy('client.userAgent') // .groupBy('client.userAgent')
.orderBy('count', 'DESC') // .orderBy('"totalHashRate"', 'DESC')
.getRawMany(); // .getRawMany();
return result; // return result;
} // }
} }
+14
View File
@@ -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;
}
+15
View File
@@ -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 { }
+45
View File
@@ -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
}
});
}
}
+15
View File
@@ -25,4 +25,19 @@ export class RpcBlockService {
public saveBlock(blockHeight: number, data: string) { public saveBlock(blockHeight: number, data: string) {
return this.rpcBlockRepository.update(blockHeight, { data }) 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;
}
} }
+3 -5
View File
@@ -1,14 +1,12 @@
import { CreateDateColumn, DeleteDateColumn, UpdateDateColumn } from 'typeorm'; import { CreateDateColumn, DeleteDateColumn, UpdateDateColumn } from 'typeorm';
import { DateTimeTransformer } from './DateTimeTransformer';
export abstract class TrackedEntity { export abstract class TrackedEntity {
@DeleteDateColumn({ nullable: true, type: 'datetime', transformer: new DateTimeTransformer() }) @DeleteDateColumn({ nullable: true, type: 'timestamp' })
public deletedAt?: Date; public deletedAt?: Date;
@CreateDateColumn({ type: 'datetime', transformer: new DateTimeTransformer() }) @CreateDateColumn({ type: 'timestamp' })
public createdAt?: Date public createdAt?: Date
@UpdateDateColumn({ type: 'datetime', transformer: new DateTimeTransformer() }) @UpdateDateColumn({ type: 'timestamp' })
public updatedAt?: Date public updatedAt?: Date
} }
+16 -8
View File
@@ -3,9 +3,12 @@ import { Controller, Get, Inject } from '@nestjs/common';
import { Cache } from 'cache-manager'; import { Cache } from 'cache-manager';
import { firstValueFrom } from 'rxjs'; 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 { BlocksService } from './ORM/blocks/blocks.service';
import { ClientStatisticsService } from './ORM/client-statistics/client-statistics.service'; import { ClientStatisticsService } from './ORM/client-statistics/client-statistics.service';
import { ClientService } from './ORM/client/client.service'; import { ClientService } from './ORM/client/client.service';
import { HomeGraphService } from './ORM/home-graph/home-graph.service';
import { BitcoinRpcService } from './services/bitcoin-rpc.service'; import { BitcoinRpcService } from './services/bitcoin-rpc.service';
@Controller() @Controller()
@@ -18,7 +21,10 @@ export class AppController {
private readonly clientService: ClientService, private readonly clientService: ClientService,
private readonly clientStatisticsService: ClientStatisticsService, private readonly clientStatisticsService: ClientStatisticsService,
private readonly blocksService: BlocksService, 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') @Get('info')
@@ -34,16 +40,18 @@ export class AppController {
const blockData = await this.blocksService.getFoundBlocks(); 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 = { const data = {
blockData, blockData,
userAgents, userAgents,
highScores,
uptime: this.uptime uptime: this.uptime
}; };
//1 min //5 min
await this.cacheManager.set(CACHE_KEY, data, 1 * 60 * 1000); await this.cacheManager.set(CACHE_KEY, data, 5 * 60 * 1000);
return data; return data;
@@ -60,9 +68,9 @@ export class AppController {
} }
const userAgents = await this.clientService.getUserAgents(); const userAgents = await this.userAgentReportService.getReport();
const totalHashRate = userAgents.reduce((acc, userAgent) => acc + parseFloat(userAgent.totalHashRate), 0); const totalHashRate = userAgents.reduce((acc, userAgent) => acc + userAgent.totalHashRate, 0);
const totalMiners = userAgents.reduce((acc, userAgent) => acc + parseInt(userAgent.count), 0); const totalMiners = userAgents.reduce((acc, userAgent) => acc + userAgent.count, 0);
const blockHeight = (await firstValueFrom(this.bitcoinRpcService.newBlock$)).blocks; const blockHeight = (await firstValueFrom(this.bitcoinRpcService.newBlock$)).blocks;
const blocksFound = await this.blocksService.getFoundBlocks(); const blocksFound = await this.blocksService.getFoundBlocks();
@@ -97,7 +105,7 @@ export class AppController {
return cachedResult; return cachedResult;
} }
const chartData = await this.clientStatisticsService.getChartDataForSite(); const chartData = await this.homeGraphService.getChartDataForSite();
//10 min //10 min
await this.cacheManager.set(CACHE_KEY, chartData, 10 * 60 * 1000); await this.cacheManager.set(CACHE_KEY, chartData, 10 * 60 * 1000);
+44 -11
View File
@@ -1,7 +1,7 @@
import { HttpModule } from '@nestjs/axios'; import { HttpModule } from '@nestjs/axios';
import { CacheModule } from '@nestjs/cache-manager'; import { CacheModule } from '@nestjs/cache-manager';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule, ConfigService } from '@nestjs/config';
import { ScheduleModule } from '@nestjs/schedule'; import { ScheduleModule } from '@nestjs/schedule';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
@@ -9,11 +9,22 @@ import { AppController } from './app.controller';
import { AddressController } from './controllers/address/address.controller'; import { AddressController } from './controllers/address/address.controller';
import { ClientController } from './controllers/client/client.controller'; import { ClientController } from './controllers/client/client.controller';
import { BitcoinAddressValidator } from './models/validators/bitcoin-address.validator'; 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 { AddressSettingsModule } from './ORM/address-settings/address-settings.module';
import { BlocksEntity } from './ORM/blocks/blocks.entity';
import { BlocksModule } from './ORM/blocks/blocks.module'; 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 { ClientStatisticsModule } from './ORM/client-statistics/client-statistics.module';
import { ClientEntity } from './ORM/client/client.entity';
import { ClientModule } from './ORM/client/client.module'; 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 { 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 { TelegramSubscriptionsModule } from './ORM/telegram-subscriptions/telegram-subscriptions.module';
import { AppService } from './services/app.service'; import { AppService } from './services/app.service';
import { BitcoinRpcService } from './services/bitcoin-rpc.service'; import { BitcoinRpcService } from './services/bitcoin-rpc.service';
@@ -33,21 +44,43 @@ const ORMModules = [
AddressSettingsModule, AddressSettingsModule,
TelegramSubscriptionsModule, TelegramSubscriptionsModule,
BlocksModule, BlocksModule,
RpcBlocksModule RpcBlocksModule,
HomeGraphModule,
UserAgentReportModule
] ]
@Module({ @Module({
imports: [ imports: [
ConfigModule.forRoot(), ConfigModule.forRoot(),
TypeOrmModule.forRoot({ TypeOrmModule.forRootAsync({
type: 'sqlite', useFactory: (configService: ConfigService) => {
database: './DB/public-pool.sqlite', return {
synchronize: true, type: 'postgres',
autoLoadEntities: true, host: configService.get('DB_HOST'),
logging: false, port: parseInt(configService.get('DB_PORT')),
enableWAL: true, username: configService.get('DB_USERNAME'),
busyTimeout: 30 * 1000, 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,
poolSize: 30,
migrations: [
UniqueNonceIndex
]
}
},
imports: [ConfigModule],
inject: [ConfigService]
}), }),
CacheModule.register(), CacheModule.register(),
ScheduleModule.forRoot(), ScheduleModule.forRoot(),
+2 -2
View File
@@ -30,7 +30,7 @@ export class ClientController {
return { return {
sessionId: worker.sessionId, sessionId: worker.sessionId,
name: worker.clientName, name: worker.clientName,
bestDifficulty: worker.bestDifficulty.toFixed(2), bestDifficulty: parseFloat(worker.bestDifficulty as any).toFixed(2),
hashRate: worker.hashRate, hashRate: worker.hashRate,
startTime: worker.startTime, startTime: worker.startTime,
lastSeen: worker.updatedAt lastSeen: worker.updatedAt
@@ -75,7 +75,7 @@ export class ClientController {
if (worker == null) { if (worker == null) {
return new NotFoundException(); return new NotFoundException();
} }
const chartData = await this.clientStatisticsService.getChartDataForSession(worker.address, worker.clientName, worker.sessionId); const chartData = await this.clientStatisticsService.getChartDataForSession(worker.id);
return { return {
sessionId: worker.sessionId, sessionId: worker.sessionId,
+24 -22
View File
@@ -43,7 +43,7 @@ export class StratumV1Client {
private usedSuggestedDifficulty = false; private usedSuggestedDifficulty = false;
private sessionDifficulty: number = 16384; private sessionDifficulty: number = 16384;
private entity: ClientEntity; private clientEntity: ClientEntity;
private creatingEntity: Promise<void>; private creatingEntity: Promise<void>;
public extraNonceAndSessionId: string; public extraNonceAndSessionId: string;
@@ -83,7 +83,9 @@ export class StratumV1Client {
public async destroy() { public async destroy() {
await this.clientService.delete(this.extraNonceAndSessionId); if (this.clientEntity?.id) {
await this.clientService.delete(this.clientEntity.id);
}
if (this.stratumSubscription != null) { if (this.stratumSubscription != null) {
this.stratumSubscription.unsubscribe(); this.stratumSubscription.unsubscribe();
@@ -335,7 +337,7 @@ export class StratumV1Client {
&& this.clientAuthorization != null && this.clientAuthorization != null
&& this.stratumInitialized == false) { && this.stratumInitialized == false) {
await this.initStratum(); this.initStratum();
} }
} }
@@ -373,11 +375,11 @@ export class StratumV1Client {
}, 60 * 1000) }, 60 * 1000)
); );
this.backgroundWork.push( // this.backgroundWork.push(
setInterval(async () => { // setInterval(async () => {
await this.statistics.saveShares(this.entity); // await this.statistics.saveShares(this.clientEntity);
}, 60 * 1000) // }, 60 * 1000)
); // );
} }
private async sendNewMiningJob(jobTemplate: IJobTemplate) { private async sendNewMiningJob(jobTemplate: IJobTemplate) {
@@ -386,8 +388,8 @@ export class StratumV1Client {
const devFeeAddress = this.configService.get('DEV_FEE_ADDRESS'); const devFeeAddress = this.configService.get('DEV_FEE_ADDRESS');
//50Th/s //50Th/s
this.noFee = false; this.noFee = false;
if (this.entity) { if (this.clientEntity) {
this.hashRate = await this.clientStatisticsService.getHashRateForSession(this.clientAuthorization.address, this.clientAuthorization.worker, this.extraNonceAndSessionId); this.hashRate = await this.clientStatisticsService.getHashRateForSession(this.clientEntity.id);
this.noFee = this.hashRate != 0 && this.hashRate < 50000000000000; this.noFee = this.hashRate != 0 && this.hashRate < 50000000000000;
} }
if (this.noFee || devFeeAddress == null || devFeeAddress.length < 1) { if (this.noFee || devFeeAddress == null || devFeeAddress.length < 1) {
@@ -438,11 +440,11 @@ export class StratumV1Client {
private async handleMiningSubmission(submission: MiningSubmitMessage) { private async handleMiningSubmission(submission: MiningSubmitMessage) {
if (this.entity == null) { if (this.clientEntity == null) {
if (this.creatingEntity == null) { if (this.creatingEntity == null) {
this.creatingEntity = new Promise(async (resolve, reject) => { this.creatingEntity = new Promise(async (resolve, reject) => {
try { try {
this.entity = await this.clientService.insert({ this.clientEntity = await this.clientService.insert({
sessionId: this.extraNonceAndSessionId, sessionId: this.extraNonceAndSessionId,
address: this.clientAuthorization.address, address: this.clientAuthorization.address,
clientName: this.clientAuthorization.worker, clientName: this.clientAuthorization.worker,
@@ -515,12 +517,12 @@ export class StratumV1Client {
} }
} }
try { try {
await this.statistics.addShares(this.sessionDifficulty); await this.statistics.addShares(this.clientEntity, this.sessionDifficulty);
const now = new Date(); const now = new Date();
// only update every minute // only update every minute
if (this.entity.updatedAt == null || now.getTime() - this.entity.updatedAt.getTime() > 1000 * 60) { if (this.clientEntity.updatedAt == null || now.getTime() - this.clientEntity.updatedAt.getTime() > 1000 * 60) {
await this.clientService.heartbeat(this.entity.address, this.entity.clientName, this.entity.sessionId, this.hashRate, now); await this.clientService.heartbeat(this.clientEntity.id, this.hashRate, now);
this.entity.updatedAt = now; this.clientEntity.updatedAt = now;
} }
} catch (e) { } catch (e) {
@@ -537,11 +539,11 @@ export class StratumV1Client {
return false; return false;
} }
if (submissionDifficulty > this.entity.bestDifficulty) { if (submissionDifficulty > this.clientEntity.bestDifficulty) {
await this.clientService.updateBestDifficulty(this.extraNonceAndSessionId, submissionDifficulty); await this.clientService.updateBestDifficulty(this.clientEntity.id, submissionDifficulty);
this.entity.bestDifficulty = submissionDifficulty; this.clientEntity.bestDifficulty = submissionDifficulty;
if (submissionDifficulty > (await this.addressSettingsService.getSettings(this.clientAuthorization.address, true)).bestDifficulty) { 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; return true;
} else { } 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(); this.destroy();
if (!this.socket.destroyed) { if (!this.socket.destroyed) {
this.socket.destroy(); this.socket.destroy();
@@ -645,7 +647,7 @@ export class StratumV1Client {
} else if (!this.socket.destroyed) { } else if (!this.socket.destroyed) {
this.socket.destroy(); 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; return false;
} }
} }
+42 -18
View File
@@ -6,10 +6,14 @@ const TARGET_SUBMISSION_PER_SECOND = 10;
const MIN_DIFF = 0.00001; const MIN_DIFF = 0.00001;
export class StratumV1ClientStatistics { export class StratumV1ClientStatistics {
private shareBacklog: number = 0; private shares: number = 0;
private acceptedCount: number = 0;
private submissionCacheStart: Date; private submissionCacheStart: Date;
private submissionCache = []; private submissionCache: { time: Date, difficulty: number }[] = [];
private currentTimeSlot: number = null;
private lastSave: number = null;
constructor( constructor(
private readonly clientStatisticsService: ClientStatisticsService private readonly clientStatisticsService: ClientStatisticsService
@@ -20,40 +24,60 @@ export class StratumV1ClientStatistics {
public async saveShares(client: ClientEntity) { public async saveShares(client: ClientEntity) {
if (client == null || client.address == null || client.clientName == null || client.sessionId == null) { // if (client == null || client.address == null || client.clientName == null || client.sessionId == null) {
return; // return;
} // }
// 10 min
var coeff = 1000 * 60 * 10;
var date = new Date();
var rounded = new Date(Math.floor(date.getTime() / coeff) * coeff);
await this.clientStatisticsService.save({ await this.clientStatisticsService.save({
time: rounded.getTime(), time: this.currentTimeSlot,
shares: this.shareBacklog, clientId: client.id,
shares: this.shares,
acceptedCount: this.acceptedCount,
address: client.address, address: client.address,
clientName: client.clientName, clientName: client.clientName,
sessionId: client.sessionId 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() // 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) { if (this.submissionCache.length > CACHE_SIZE) {
this.submissionCache.shift(); this.submissionCache.shift();
} }
this.submissionCache.push({ this.submissionCache.push({
time: new Date(), time: date,
difficulty: targetDifficulty, 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++;
} }
+73 -26
View File
@@ -1,9 +1,11 @@
import { Injectable, OnModuleInit } from '@nestjs/common'; import { Injectable, OnModuleInit } from '@nestjs/common';
import { Interval } from '@nestjs/schedule';
import { DataSource } from 'typeorm'; 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 { ClientStatisticsService } from '../ORM/client-statistics/client-statistics.service';
import { ClientService } from '../ORM/client/client.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() @Injectable()
export class AppService implements OnModuleInit { export class AppService implements OnModuleInit {
@@ -11,42 +13,87 @@ export class AppService implements OnModuleInit {
constructor( constructor(
private readonly clientStatisticsService: ClientStatisticsService, private readonly clientStatisticsService: ClientStatisticsService,
private readonly clientService: ClientService, 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() { async onModuleInit() {
// if (process.env.NODE_APP_INSTANCE == '0') { if (process.env.ENABLE_SOLO == 'true' && (process.env.NODE_APP_INSTANCE == null || process.env.NODE_APP_INSTANCE == '0')) {
// await this.dataSource.query(`VACUUM;`);
// } 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() { private async deleteOldStatistics() {
console.log('Deleting statistics'); 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(); const deletedStatistics = await this.clientStatisticsService.deleteOldStatistics();
console.log(`Deleted ${deletedStatistics.affected} old statistics`); console.log(`Deleted ${deletedStatistics.affected} old statistics`);
const deletedClients = await this.clientService.deleteOldClients(); const deletedClients = await this.clientService.deleteOldClients();
console.log(`Deleted ${deletedClients.affected} old clients`); 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);
}
} }
+31 -10
View File
@@ -1,15 +1,15 @@
import { Injectable } from '@nestjs/common'; import { Injectable, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { RPCClient } from 'rpc-bitcoin'; import { RPCClient } from 'rpc-bitcoin';
import { BehaviorSubject, filter, shareReplay } from 'rxjs'; import { BehaviorSubject, filter, shareReplay } from 'rxjs';
import { RpcBlockService } from 'src/ORM/rpc-block/rpc-block.service'; 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 { IBlockTemplate } from '../models/bitcoin-rpc/IBlockTemplate';
import { IMiningInfo } from '../models/bitcoin-rpc/IMiningInfo'; import { IMiningInfo } from '../models/bitcoin-rpc/IMiningInfo';
@Injectable() @Injectable()
export class BitcoinRpcService { export class BitcoinRpcService implements OnModuleInit {
private blockHeight = 0; private blockHeight = 0;
private client: RPCClient; private client: RPCClient;
@@ -21,6 +21,10 @@ export class BitcoinRpcService {
private rpcBlockService: RpcBlockService private rpcBlockService: RpcBlockService
) { ) {
}
async onModuleInit() {
const url = this.configService.get('BITCOIN_RPC_URL'); const url = this.configService.get('BITCOIN_RPC_URL');
const user = this.configService.get('BITCOIN_RPC_USER'); const user = this.configService.get('BITCOIN_RPC_USER');
const pass = this.configService.get('BITCOIN_RPC_PASSWORD'); const pass = this.configService.get('BITCOIN_RPC_PASSWORD');
@@ -36,19 +40,36 @@ export class BitcoinRpcService {
}); });
if (this.configService.get('BITCOIN_ZMQ_HOST')) { if (this.configService.get('BITCOIN_ZMQ_HOST')) {
const sock = zmq.socket("sub"); console.log('Using ZMQ');
sock.connect(this.configService.get('BITCOIN_ZMQ_HOST')); const sock = new zmq.Subscriber;
sock.subscribe("rawblock");
sock.on("message", async (topic: Buffer, message: Buffer) => {
console.log("new block zmq"); sock.connectTimeout = 1000;
await this.pollMiningInfo(); 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 { } else {
setInterval(this.pollMiningInfo.bind(this), 500); 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() { public async pollMiningInfo() {
const miningInfo = await this.getMiningInfo(); const miningInfo = await this.getMiningInfo();
if (miningInfo != null && miningInfo.blocks > this.blockHeight) { if (miningInfo != null && miningInfo.blocks > this.blockHeight) {