This commit is contained in:
PhilReact 2025-05-04 20:31:43 +03:00
parent bbfd29e8fb
commit f32cd68554
8 changed files with 650 additions and 225 deletions

59
package-lock.json generated
View File

@ -19,7 +19,7 @@
"jotai": "^2.12.3", "jotai": "^2.12.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.30.1", "moment": "^2.30.1",
"qapp-core": "^1.0.24", "qapp-core": "^1.0.25",
"react": "^19.1.0", "react": "^19.1.0",
"react-countdown-circle-timer": "^3.2.1", "react-countdown-circle-timer": "^3.2.1",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
@ -3128,6 +3128,7 @@
"version": "3.13.6", "version": "3.13.6",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.6.tgz", "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.6.tgz",
"integrity": "sha512-WT7nWs8ximoQ0CDx/ngoFP7HbQF9Q2wQe4nh2NB+u2486eX3nZRE40P9g6ccCVq7ZfTSH5gFOuCoVH5DLNS/aA==", "integrity": "sha512-WT7nWs8ximoQ0CDx/ngoFP7HbQF9Q2wQe4nh2NB+u2486eX3nZRE40P9g6ccCVq7ZfTSH5gFOuCoVH5DLNS/aA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@tanstack/virtual-core": "3.13.6" "@tanstack/virtual-core": "3.13.6"
}, },
@ -3144,6 +3145,7 @@
"version": "3.13.6", "version": "3.13.6",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.6.tgz", "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.6.tgz",
"integrity": "sha512-cnQUeWnhNP8tJ4WsGcYiX24Gjkc9ALstLbHcBj1t3E7EimN6n6kHH+DPV4PpDnuw00NApQp+ViojMj1GRdwYQg==", "integrity": "sha512-cnQUeWnhNP8tJ4WsGcYiX24Gjkc9ALstLbHcBj1t3E7EimN6n6kHH+DPV4PpDnuw00NApQp+ViojMj1GRdwYQg==",
"license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/tannerlinsley" "url": "https://github.com/sponsors/tannerlinsley"
@ -3254,7 +3256,8 @@
"node_modules/@types/seedrandom": { "node_modules/@types/seedrandom": {
"version": "3.0.8", "version": "3.0.8",
"resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-3.0.8.tgz", "resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-3.0.8.tgz",
"integrity": "sha512-TY1eezMU2zH2ozQoAFAQFOPpvP15g+ZgSfTZt31AUUH/Rxtnz3H+A/Sv1Snw2/amp//omibc+AEkTaA8KUeOLQ==" "integrity": "sha512-TY1eezMU2zH2ozQoAFAQFOPpvP15g+ZgSfTZt31AUUH/Rxtnz3H+A/Sv1Snw2/amp//omibc+AEkTaA8KUeOLQ==",
"license": "MIT"
}, },
"node_modules/@types/semver": { "node_modules/@types/semver": {
"version": "7.5.8", "version": "7.5.8",
@ -3661,6 +3664,7 @@
"version": "2.2.5", "version": "2.2.5",
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
"integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==", "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==",
"license": "MIT",
"engines": { "engines": {
"node": ">=4" "node": ">=4"
} }
@ -3762,6 +3766,7 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.6.0" "node": ">= 0.6.0"
} }
@ -3783,7 +3788,8 @@
"type": "consulting", "type": "consulting",
"url": "https://feross.org/support" "url": "https://feross.org/support"
} }
] ],
"license": "MIT"
}, },
"node_modules/binary-extensions": { "node_modules/binary-extensions": {
"version": "2.3.0", "version": "2.3.0",
@ -3800,6 +3806,7 @@
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/bloom-filters/-/bloom-filters-3.0.4.tgz", "resolved": "https://registry.npmjs.org/bloom-filters/-/bloom-filters-3.0.4.tgz",
"integrity": "sha512-BdnPWo2OpYhlvuP2fRzJBdioMCkm7Zp0HCf8NJgF5Mbyqy7VQ/CnTiVWMMyq4EZCBHwj0Kq6098gW2/3RsZsrA==", "integrity": "sha512-BdnPWo2OpYhlvuP2fRzJBdioMCkm7Zp0HCf8NJgF5Mbyqy7VQ/CnTiVWMMyq4EZCBHwj0Kq6098gW2/3RsZsrA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@types/seedrandom": "^3.0.8", "@types/seedrandom": "^3.0.8",
"base64-arraybuffer": "^1.0.2", "base64-arraybuffer": "^1.0.2",
@ -3817,7 +3824,8 @@
"node_modules/blueimp-canvas-to-blob": { "node_modules/blueimp-canvas-to-blob": {
"version": "3.29.0", "version": "3.29.0",
"resolved": "https://registry.npmjs.org/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.29.0.tgz", "resolved": "https://registry.npmjs.org/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.29.0.tgz",
"integrity": "sha512-0pcSSGxC0QxT+yVkivxIqW0Y4VlO2XSDPofBAqoJ1qJxgH9eiUDLv50Rixij2cDuEfx4M6DpD9UGZpRhT5Q8qg==" "integrity": "sha512-0pcSSGxC0QxT+yVkivxIqW0Y4VlO2XSDPofBAqoJ1qJxgH9eiUDLv50Rixij2cDuEfx4M6DpD9UGZpRhT5Q8qg==",
"license": "MIT"
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "2.0.1", "version": "2.0.1",
@ -3889,6 +3897,7 @@
"url": "https://feross.org/support" "url": "https://feross.org/support"
} }
], ],
"license": "MIT",
"dependencies": { "dependencies": {
"base64-js": "^1.3.1", "base64-js": "^1.3.1",
"ieee754": "^1.2.1" "ieee754": "^1.2.1"
@ -4066,6 +4075,7 @@
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/compressorjs/-/compressorjs-1.2.1.tgz", "resolved": "https://registry.npmjs.org/compressorjs/-/compressorjs-1.2.1.tgz",
"integrity": "sha512-+geIjeRnPhQ+LLvvA7wxBQE5ddeLU7pJ3FsKFWirDw6veY3s9iLxAQEw7lXGHnhCJvBujEQWuNnGzZcvCvdkLQ==", "integrity": "sha512-+geIjeRnPhQ+LLvvA7wxBQE5ddeLU7pJ3FsKFWirDw6veY3s9iLxAQEw7lXGHnhCJvBujEQWuNnGzZcvCvdkLQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"blueimp-canvas-to-blob": "^3.29.0", "blueimp-canvas-to-blob": "^3.29.0",
"is-blob": "^2.1.0" "is-blob": "^2.1.0"
@ -4128,7 +4138,8 @@
"node_modules/crypto-js": { "node_modules/crypto-js": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT"
}, },
"node_modules/crypto-random-string": { "node_modules/crypto-random-string": {
"version": "2.0.0", "version": "2.0.0",
@ -4167,7 +4178,8 @@
"node_modules/cuint": { "node_modules/cuint": {
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz", "resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz",
"integrity": "sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==" "integrity": "sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==",
"license": "MIT"
}, },
"node_modules/data-view-buffer": { "node_modules/data-view-buffer": {
"version": "1.0.1", "version": "1.0.1",
@ -4223,7 +4235,8 @@
"node_modules/dayjs": { "node_modules/dayjs": {
"version": "1.11.13", "version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"license": "MIT"
}, },
"node_modules/debug": { "node_modules/debug": {
"version": "4.3.7", "version": "4.3.7",
@ -4342,6 +4355,7 @@
"version": "3.2.5", "version": "3.2.5",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.5.tgz", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.5.tgz",
"integrity": "sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ==", "integrity": "sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": { "optionalDependencies": {
"@types/trusted-types": "^2.0.7" "@types/trusted-types": "^2.0.7"
} }
@ -4944,6 +4958,7 @@
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz", "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz",
"integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==", "integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==",
"license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^2.7.0" "tslib": "^2.7.0"
}, },
@ -4954,7 +4969,8 @@
"node_modules/file-selector/node_modules/tslib": { "node_modules/file-selector/node_modules/tslib": {
"version": "2.8.1", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
}, },
"node_modules/filelist": { "node_modules/filelist": {
"version": "1.0.4", "version": "1.0.4",
@ -5292,6 +5308,7 @@
"version": "2.1.16", "version": "2.1.16",
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz",
"integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==",
"license": "MIT",
"peerDependencies": { "peerDependencies": {
"csstype": "^3.0.10" "csstype": "^3.0.10"
} }
@ -5435,7 +5452,8 @@
"type": "consulting", "type": "consulting",
"url": "https://feross.org/support" "url": "https://feross.org/support"
} }
] ],
"license": "BSD-3-Clause"
}, },
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.3.1", "version": "5.3.1",
@ -5553,6 +5571,7 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-blob/-/is-blob-2.1.0.tgz", "resolved": "https://registry.npmjs.org/is-blob/-/is-blob-2.1.0.tgz",
"integrity": "sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw==", "integrity": "sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw==",
"license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
}, },
@ -5594,6 +5613,7 @@
"url": "https://feross.org/support" "url": "https://feross.org/support"
} }
], ],
"license": "MIT",
"engines": { "engines": {
"node": ">=4" "node": ">=4"
} }
@ -6162,7 +6182,8 @@
"node_modules/long": { "node_modules/long": {
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==" "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0"
}, },
"node_modules/loose-envify": { "node_modules/loose-envify": {
"version": "1.4.0", "version": "1.4.0",
@ -6581,9 +6602,9 @@
} }
}, },
"node_modules/qapp-core": { "node_modules/qapp-core": {
"version": "1.0.24", "version": "1.0.25",
"resolved": "https://registry.npmjs.org/qapp-core/-/qapp-core-1.0.24.tgz", "resolved": "https://registry.npmjs.org/qapp-core/-/qapp-core-1.0.25.tgz",
"integrity": "sha512-KnrwiysaHlTR1rUnPGwN79gQgcjvtLr4xRH3EGGQcbXKCcbrklV7lv4KLUfFR4UwhskbgOXpJY5PECWdrlGXSw==", "integrity": "sha512-JhdQliLPjapgzbCjkuSpR5hAk8ewT0FbQxIW+oxVWJyT1e0swAKXJqvX1PHQzTbiV5IQI12nbIxy6SjBam3K2A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tanstack/react-virtual": "^3.13.2", "@tanstack/react-virtual": "^3.13.2",
@ -6675,6 +6696,7 @@
"version": "14.3.8", "version": "14.3.8",
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz", "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz",
"integrity": "sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==", "integrity": "sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==",
"license": "MIT",
"dependencies": { "dependencies": {
"attr-accept": "^2.2.4", "attr-accept": "^2.2.4",
"file-selector": "^2.1.0", "file-selector": "^2.1.0",
@ -6696,6 +6718,7 @@
"version": "2.5.2", "version": "2.5.2",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.2.tgz", "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.2.tgz",
"integrity": "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw==", "integrity": "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw==",
"license": "MIT",
"dependencies": { "dependencies": {
"csstype": "^3.1.3", "csstype": "^3.1.3",
"goober": "^2.1.16" "goober": "^2.1.16"
@ -6712,6 +6735,7 @@
"version": "9.16.0", "version": "9.16.0",
"resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz", "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz",
"integrity": "sha512-w9nJSEp+DrW9KmQmeWHQyfaP6b03v+TdXynaoA964Wxt7mdR3An11z4NNCQgL4gKSK7y1ver2Fq+JKH6CWEzUA==", "integrity": "sha512-w9nJSEp+DrW9KmQmeWHQyfaP6b03v+TdXynaoA964Wxt7mdR3An11z4NNCQgL4gKSK7y1ver2Fq+JKH6CWEzUA==",
"license": "MIT",
"peerDependencies": { "peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
@ -6836,7 +6860,8 @@
"node_modules/reflect-metadata": { "node_modules/reflect-metadata": {
"version": "0.1.14", "version": "0.1.14",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz",
"integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==" "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==",
"license": "Apache-2.0"
}, },
"node_modules/regenerate": { "node_modules/regenerate": {
"version": "1.4.2", "version": "1.4.2",
@ -7122,7 +7147,8 @@
"node_modules/seedrandom": { "node_modules/seedrandom": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==" "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==",
"license": "MIT"
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "7.6.0", "version": "7.6.0",
@ -7906,6 +7932,7 @@
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
"license": "MIT",
"peerDependencies": { "peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
} }
@ -8434,6 +8461,7 @@
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/xxhashjs/-/xxhashjs-0.2.2.tgz", "resolved": "https://registry.npmjs.org/xxhashjs/-/xxhashjs-0.2.2.tgz",
"integrity": "sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==", "integrity": "sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==",
"license": "MIT",
"dependencies": { "dependencies": {
"cuint": "^0.2.2" "cuint": "^0.2.2"
} }
@ -8468,6 +8496,7 @@
"version": "4.5.6", "version": "4.5.6",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz", "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz",
"integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==", "integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"use-sync-external-store": "^1.2.2" "use-sync-external-store": "^1.2.2"
}, },

View File

@ -21,7 +21,7 @@
"jotai": "^2.12.3", "jotai": "^2.12.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.30.1", "moment": "^2.30.1",
"qapp-core": "^1.0.24", "qapp-core": "^1.0.25",
"react": "^19.1.0", "react": "^19.1.0",
"react-countdown-circle-timer": "^3.2.1", "react-countdown-circle-timer": "^3.2.1",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",

View File

@ -61,6 +61,7 @@ import CloseIcon from "@mui/icons-material/Close";
import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import moment from "moment"; import moment from "moment";
import { RequestQueueWithPromise } from "qapp-core"; import { RequestQueueWithPromise } from "qapp-core";
import { useUpdateFee } from "../../hooks/useUpdateFee";
const copyToClipboard = (text: string) => { const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text); navigator.clipboard.writeText(text);
@ -91,6 +92,7 @@ export const autoSizeStrategy: SizeColumnsToContentStrategy = {
export const TradeOffers: React.FC<any> = ({ export const TradeOffers: React.FC<any> = ({
foreignCoinBalance, foreignCoinBalance,
fee, fee,
setFee
}: any) => { }: any) => {
const [offers, setOffers] = useState<any[]>([]); const [offers, setOffers] = useState<any[]>([]);
const [signedUnlockingFees, setSignedUnlockingFees] = useState(null); const [signedUnlockingFees, setSignedUnlockingFees] = useState(null);
@ -105,6 +107,7 @@ export const TradeOffers: React.FC<any> = ({
} = useContext(gameContext); } = useContext(gameContext);
const isRemoveOrdersWithoutUnlockingFees = useRef(false); const isRemoveOrdersWithoutUnlockingFees = useRef(false);
const [isRemoveOrders, setIsRemoveOrders] = useState('remove'); const [isRemoveOrders, setIsRemoveOrders] = useState('remove');
const updateFee = useUpdateFee({setFee, selectedCoin})
const listOfOngoingTradesAts = useMemo(() => { const listOfOngoingTradesAts = useMemo(() => {
return ( return (
onGoingTrades onGoingTrades
@ -128,6 +131,14 @@ export const TradeOffers: React.FC<any> = ({
message: messageTradesUnknownFee, message: messageTradesUnknownFee,
} = useModal(); } = useModal();
const {
isShow: isShowAskToUpdateFee,
onCancel: onCancelAskToUpdateFee,
onOk: onOkAskToUpdateFee,
show: showAskToUpdateFee,
message: messageAskToUpdateFee,
} = useModal();
const offersWithoutOngoing = useMemo(() => { const offersWithoutOngoing = useMemo(() => {
return offers.filter( return offers.filter(
(item) => !listOfOngoingTradesAts.includes(item.qortalAtAddress) (item) => !listOfOngoingTradesAts.includes(item.qortalAtAddress)
@ -228,6 +239,20 @@ export const TradeOffers: React.FC<any> = ({
} }
}; };
const rowTooltip = (params) => {
let selectable = true;
const hasSignedFee = signedUnlockingFees?.find(
(item) => item?.atAddress === params.data.qortalAtAddress
);
if (!hasSignedFee) selectable = true;
if (hasSignedFee && hasSignedFee?.fee > feeRef.current)
selectable = false;
if(!selectable)return 'Your unlocking fee is to low to buy this order, Increase your fee to purchase'
return ''
};
const columnDefs: ColDef[] = useMemo(() => { const columnDefs: ColDef[] = useMemo(() => {
return [ return [
{ {
@ -239,7 +264,7 @@ export const TradeOffers: React.FC<any> = ({
pinned: "left", // Optional, to pin this column on the left pinned: "left", // Optional, to pin this column on the left
resizable: false, resizable: false,
suppressRowClickSelection: true, suppressRowClickSelection: true,
tooltipValueGetter: rowTooltip,
cellRenderer: (params) => ( cellRenderer: (params) => (
<SelectWithInfoCell <SelectWithInfoCell
{...params} {...params}
@ -265,6 +290,7 @@ export const TradeOffers: React.FC<any> = ({
flex: 1, // Flex makes this column responsive flex: 1, // Flex makes this column responsive
minWidth: 150, // Ensure it doesn't shrink too much minWidth: 150, // Ensure it doesn't shrink too much
resizable: true, resizable: true,
tooltipValueGetter: rowTooltip
}, },
{ {
headerName: `${getCoinLabel()}/QORT`, headerName: `${getCoinLabel()}/QORT`,
@ -275,6 +301,7 @@ export const TradeOffers: React.FC<any> = ({
flex: 1, // Flex makes this column responsive flex: 1, // Flex makes this column responsive
minWidth: 150, // Ensure it doesn't shrink too much minWidth: 150, // Ensure it doesn't shrink too much
resizable: true, resizable: true,
tooltipValueGetter: rowTooltip
}, },
{ {
headerName: `Total ${getCoinLabel()} Value`, headerName: `Total ${getCoinLabel()} Value`,
@ -282,12 +309,14 @@ export const TradeOffers: React.FC<any> = ({
flex: 1, // Flex makes this column responsive flex: 1, // Flex makes this column responsive
minWidth: 150, // Ensure it doesn't shrink too much minWidth: 150, // Ensure it doesn't shrink too much
resizable: true, resizable: true,
tooltipValueGetter: rowTooltip
}, },
{ {
headerName: `Unlocking fee`, headerName: `Unlocking fee`,
flex: 1, // Flex makes this column responsive flex: 1, // Flex makes this column responsive
minWidth: 150, // Ensure it doesn't shrink too much minWidth: 150, // Ensure it doesn't shrink too much
resizable: true, resizable: true,
tooltipValueGetter: rowTooltip,
valueGetter: (params) => { valueGetter: (params) => {
if (params?.data?.qortalAtAddress) { if (params?.data?.qortalAtAddress) {
const hasSignedFee = signedUnlockingFees?.find( const hasSignedFee = signedUnlockingFees?.find(
@ -304,6 +333,7 @@ export const TradeOffers: React.FC<any> = ({
flex: 1, // Flex makes this column responsive flex: 1, // Flex makes this column responsive
minWidth: 300, // Ensure it doesn't shrink too much minWidth: 300, // Ensure it doesn't shrink too much
resizable: true, resizable: true,
tooltipValueGetter: rowTooltip,
valueGetter: (params) => { valueGetter: (params) => {
if (params?.data?.qortalCreator) { if (params?.data?.qortalCreator) {
if (qortalNames[params?.data?.qortalCreator]) { if (qortalNames[params?.data?.qortalCreator]) {
@ -660,6 +690,20 @@ export const TradeOffers: React.FC<any> = ({
offersWithKnownFees = selectedOffers; offersWithKnownFees = selectedOffers;
} }
const highestFee = offersWithKnownFees.length
? Math.max(
...offersWithKnownFees
.filter(o => typeof o.fee === 'number')
.map(o => o.fee as number)
)
: 0;
if(highestFee < fee){
await showAskToUpdateFee({
message: highestFee
})
}
setIsShowBuyInProgress({ status: "buying" }); setIsShowBuyInProgress({ status: "buying" });
@ -864,6 +908,7 @@ export const TradeOffers: React.FC<any> = ({
onGridReady={onGridReady} onGridReady={onGridReady}
// domLayout='autoHeight' // domLayout='autoHeight'
getRowId={(params) => params.data.qortalAtAddress} // Ensure rows have unique IDs getRowId={(params) => params.data.qortalAtAddress} // Ensure rows have unique IDs
enableBrowserTooltips={true}
gridOptions={{ gridOptions={{
isRowSelectable: (params) => { isRowSelectable: (params) => {
let selectable = true; let selectable = true;
@ -1001,7 +1046,7 @@ export const TradeOffers: React.FC<any> = ({
</Button> </Button>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button variant="contained" onClick={onOkInfo} autoFocus> <Button variant="outlined" onClick={onOkInfo} autoFocus>
Close Close
</Button> </Button>
</DialogActions> </DialogActions>
@ -1012,9 +1057,19 @@ export const TradeOffers: React.FC<any> = ({
open={isShowTradesUnknownFee} open={isShowTradesUnknownFee}
aria-labelledby="alert-dialog-title" aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description" aria-describedby="alert-dialog-description"
PaperProps={{
style: {
backgroundColor: "rgb(39, 40, 44)",
background: "rgb(39, 40, 44)",
},
}}
> >
<DialogTitle id="alert-dialog-title">Warning</DialogTitle> <DialogTitle sx={{
<DialogContent>
background: "rgb(39, 40, 44)"
}} id="alert-dialog-title">Warning</DialogTitle>
<DialogContent sx={{ borderColor: "#333" }}>
<DialogContentText <DialogContentText
id="alert-dialog-description" id="alert-dialog-description"
sx={{ color: "white" }} sx={{ color: "white" }}
@ -1076,16 +1131,18 @@ export const TradeOffers: React.FC<any> = ({
</DialogContent> </DialogContent>
<DialogActions> <DialogActions sx={{
background: "rgb(39, 40, 44)",
}}>
<Button <Button
variant="contained" variant="outlined"
onClick={onCancelTradesUnknownFee} onClick={onCancelTradesUnknownFee}
autoFocus autoFocus
> >
Close Close
</Button> </Button>
<Button <Button
variant="contained" variant="outlined"
onClick={onOkTradesUnknownFee} onClick={onOkTradesUnknownFee}
autoFocus autoFocus
> >
@ -1094,6 +1151,61 @@ export const TradeOffers: React.FC<any> = ({
</DialogActions> </DialogActions>
</Dialog> </Dialog>
)} )}
{isShowAskToUpdateFee && (
<Dialog
open={isShowAskToUpdateFee}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
PaperProps={{
style: {
backgroundColor: "rgb(39, 40, 44)",
background: "rgb(39, 40, 44)",
},
}}
>
<DialogTitle sx={{
background: "rgb(39, 40, 44)",
}} id="alert-dialog-title">Suggestion</DialogTitle>
<DialogContent sx={{ borderColor: "#333" }}>
<DialogContentText
id="alert-dialog-description"
sx={{ color: "white" }}
>
Your current unlocking fee is higher than necessary. You can lower it to match the highest required fee and reduce costs.
</DialogContentText>
<Spacer height="20px" />
</DialogContent>
<DialogActions sx={{
background: "rgb(39, 40, 44)",
}}>
<Button
variant="outlined"
onClick={onOkAskToUpdateFee}
>
Continue without updating
</Button>
<Button
variant="outlined"
onClick={async (e)=> {
try {
await updateFee(messageAskToUpdateFee.message)
} catch (error) {
console.error(error)
}
onOkAskToUpdateFee(e)
}}
>
Lower fee
</Button>
</DialogActions>
</Dialog>
)}
{isShowBuyInProgress && ( {isShowBuyInProgress && (
<Dialog <Dialog
open={isShowBuyInProgress} open={isShowBuyInProgress}

View File

@ -30,7 +30,11 @@ import { usePublish, Service, QortalGetMetadata } from "qapp-core";
import { SetLeftFeature } from "ag-grid-community"; import { SetLeftFeature } from "ag-grid-community";
import { formatTimestampForum } from "../../utils/formatTime"; import { formatTimestampForum } from "../../utils/formatTime";
import { useAtom } from "jotai/react"; import { useAtom } from "jotai/react";
import { isEnabledCustomLockingFeeAtom, selectedFeePublisherAtom } from "../../global/state"; import {
isEnabledCustomLockingFeeAtom,
selectedFeePublisherAtom,
} from "../../global/state";
import { useRecommendedFees } from "../../hooks/useRecommendedFees";
type FeeEstimate = { type FeeEstimate = {
height: number; height: number;
@ -39,15 +43,15 @@ type FeeEstimate = {
medium_fee_per_kb: number; medium_fee_per_kb: number;
high_fee_per_kb: number; high_fee_per_kb: number;
}; };
function isValidFeeEstimate(obj: any): obj is FeeEstimate { export function isValidFeeEstimate(obj: any): obj is FeeEstimate {
return ( return (
typeof obj === 'object' && typeof obj === "object" &&
obj !== null && obj !== null &&
typeof obj.height === 'number' && typeof obj.height === "number" &&
typeof obj.time === 'number' && typeof obj.time === "number" &&
typeof obj.low_fee_per_kb === 'number' && typeof obj.low_fee_per_kb === "number" &&
typeof obj.medium_fee_per_kb === 'number' && typeof obj.medium_fee_per_kb === "number" &&
typeof obj.high_fee_per_kb === 'number' typeof obj.high_fee_per_kb === "number"
); );
} }
@ -56,9 +60,9 @@ function calculateFeeFromRate(feePerKb, sizeInBytes) {
return fee?.toFixed(0); return fee?.toFixed(0);
} }
function calculateRateFromFee(totalFee, sizeInBytes) { export function calculateRateFromFee(totalFee, sizeInBytes) {
const fee = (totalFee / sizeInBytes) * 1000; const fee = (totalFee / sizeInBytes) * 1000;
return fee.toFixed(0) return fee.toFixed(0);
} }
export const FeeManager = ({ selectedCoin, setFee, fee }) => { export const FeeManager = ({ selectedCoin, setFee, fee }) => {
@ -68,12 +72,19 @@ export const FeeManager = ({ selectedCoin, setFee, fee }) => {
service: "JSON", service: "JSON",
}); });
const { resource } = usePublish(3, "JSON", feeLocation); const { resource } = usePublish(3, "JSON", feeLocation);
const [selectedFeePublisher, setSelectedFeePublisher] = useAtom(selectedFeePublisherAtom) const [selectedFeePublisher, setSelectedFeePublisher] = useAtom(
const [isEnabledCustomLockingFee, setIsEnabledCustomLockingFee] = useAtom(isEnabledCustomLockingFeeAtom) selectedFeePublisherAtom
);
const [isEnabledCustomLockingFee, setIsEnabledCustomLockingFee] = useAtom(
isEnabledCustomLockingFeeAtom
);
const [editFee, setEditFee] = useState(""); const [editFee, setEditFee] = useState("");
const [openModal, setOpenModal] = useState(false); const [openModal, setOpenModal] = useState(false);
const [recommendedFee, setRecommendedFee] = useState("m"); const [recommendedFee, setRecommendedFee] = useState("medium_fee_per_kb");
const {hideRecommendations, recommendedFeeDisplay} = useRecommendedFees({selectedCoin, recommendedFee})
const [openAlert, setOpenAlert] = useState(false); const [openAlert, setOpenAlert] = useState(false);
const [info, setInfo] = useState<any>(null); const [info, setInfo] = useState<any>(null);
const { getCoinLabel } = useContext(gameContext); const { getCoinLabel } = useContext(gameContext);
@ -89,16 +100,18 @@ export const FeeManager = ({ selectedCoin, setFee, fee }) => {
setInfo(null); setInfo(null);
}; };
const coin = useMemo(() => { const coin = useMemo(() => {
const coinLabel = getCoinLabel(selectedCoin) const coinLabel = getCoinLabel(selectedCoin);
if(typeof coinLabel !== 'string') return null if (typeof coinLabel !== "string") return null;
return coinLabel?.toLowerCase(); return coinLabel?.toLowerCase();
}, [selectedCoin, getCoinLabel]); }, [selectedCoin, getCoinLabel]);
const feeTimestamp = useMemo(() => { const feeTimestamp = useMemo(() => {
if(!resource?.qortalMetadata?.identifier?.includes(`${coin.toUpperCase()}`)) return if (
return resource?.data?.time || null !resource?.qortalMetadata?.identifier?.includes(`${coin.toUpperCase()}`)
}, [resource, coin]) )
return;
return resource?.data?.time || null;
}, [resource, coin]);
const establishUpdateFeeForm = useCallback(async (coin) => { const establishUpdateFeeForm = useCallback(async (coin) => {
setFee(""); setFee("");
// if the coin or type is not set, then abort // if the coin or type is not set, then abort
@ -117,10 +130,9 @@ export const FeeManager = ({ selectedCoin, setFee, fee }) => {
}, },
1800000 1800000
); );
if ((response !== null && response !== undefined) && !isNaN(+response)) { if (response !== null && response !== undefined && !isNaN(+response)) {
setFee(response); setFee(response);
} }
} catch (error) { } catch (error) {
setFee(""); setFee("");
console.error(error); console.error(error);
@ -131,41 +143,28 @@ export const FeeManager = ({ selectedCoin, setFee, fee }) => {
establishUpdateFeeForm(coin); establishUpdateFeeForm(coin);
}, [coin, establishUpdateFeeForm]); }, [coin, establishUpdateFeeForm]);
const recommendedFeeData = useMemo(() => {
if(!resource?.qortalMetadata?.identifier?.includes(`${coin.toUpperCase()}`)) return
if (!resource?.data) return null;
const isValid = isValidFeeEstimate(resource.data)
if(!isValid) return null
return resource.data;
}, [resource, coin]);
const recommendedFeeDisplay = useMemo(() => {
if (!recommendedFeeData) return null;
if(!recommendedFeeData) return null
return recommendedFeeData[recommendedFee] || null;
}, [recommendedFeeData, recommendedFee]);
const hideRecommendations = useMemo(()=> {
if(recommendedFeeData) return false
return true
}, [recommendedFeeData])
useEffect(() => { useEffect(() => {
if (hideRecommendations) { if (hideRecommendations) {
setRecommendedFee('custom') setRecommendedFee("custom");
} }
}, [hideRecommendations]) }, [hideRecommendations]);
const updateFee = async () => { const updateFee = async () => {
const typeRequest = "feerequired"; const typeRequest = "feerequired";
const typeRequestLocking = "feekb"; const typeRequestLocking = "feekb";
try { try {
let feeToSave = editFee
if(recommendedFee !== 'custom'){ let feeToSave = editFee;
feeToSave = calculateFeeFromRate(recommendedFeeDisplay, 300) if (recommendedFee !== "custom") {
feeToSave = calculateFeeFromRate(recommendedFeeDisplay, 300);
}
if(+fee === +feeToSave){
return
} }
const response = await qortalRequestWithTimeout( const response = await qortalRequestWithTimeout(
{ {
@ -187,7 +186,6 @@ export const FeeManager = ({ selectedCoin, setFee, fee }) => {
}, },
1800000 1800000
); );
} }
if (response && !isNaN(+response)) { if (response && !isNaN(+response)) {
@ -219,29 +217,6 @@ export const FeeManager = ({ selectedCoin, setFee, fee }) => {
const getLatestFees = useCallback(async () => {
try {
const coinLabel = getCoinLabel(selectedCoin)
if(typeof coinLabel !== 'string') return
const coin = coinLabel?.toUpperCase();
const identifier = `coinInfo-${coin}`
const res = await fetch(
`/arbitrary/resources/searchsimple?service=JSON&identifier=${identifier}&name=${selectedFeePublisher}&prefix=true&limit=1&reverse=true`
);
const data = await res.json();
if (data && data?.length > 0) {
setFeeLocation(data[0]);
}
} catch (error) {
console.error(error)
}
}, [selectedFeePublisher, selectedCoin]);
useEffect(() => {
getLatestFees();
}, [getLatestFees]);
if (fee === null || fee === undefined) return; if (fee === null || fee === undefined) return;
return ( return (
<> <>
@ -281,9 +256,9 @@ export const FeeManager = ({ selectedCoin, setFee, fee }) => {
open={openModal} open={openModal}
backdrop backdrop
styles={{ styles={{
width: '450px', width: "450px",
maxWidth: '95vw', maxWidth: "95vw",
padding: '15px' padding: "15px",
}} }}
> >
<CoinActionContainer> <CoinActionContainer>
@ -301,18 +276,25 @@ export const FeeManager = ({ selectedCoin, setFee, fee }) => {
</CoinActionRow> </CoinActionRow>
<CoinActionRow> <CoinActionRow>
<HeaderRow> <HeaderRow>
<Box sx={{ <Box
width: '100%' sx={{
}}> width: "100%",
<Box sx={{ }}
width: '100%', >
display: 'flex', <Box
flexDirection: 'column', sx={{
alignItems: 'center' width: "100%",
}}> display: "flex",
<CustomLabel sx={{ flexDirection: "column",
fontSize: '16px' alignItems: "center",
}} htmlFor="standard-adornment-name"> }}
>
<CustomLabel
sx={{
fontSize: "16px",
}}
htmlFor="standard-adornment-name"
>
Recommended fee selection (in sats) Recommended fee selection (in sats)
</CustomLabel> </CustomLabel>
@ -326,9 +308,15 @@ export const FeeManager = ({ selectedCoin, setFee, fee }) => {
> >
{!hideRecommendations && ( {!hideRecommendations && (
<> <>
<ToggleButton value="low_fee_per_kb">Low</ToggleButton> <ToggleButton value="low_fee_per_kb">
<ToggleButton value="medium_fee_per_kb">Medium</ToggleButton> Low
<ToggleButton value="high_fee_per_kb">High</ToggleButton> </ToggleButton>
<ToggleButton value="medium_fee_per_kb">
Medium
</ToggleButton>
<ToggleButton value="high_fee_per_kb">
High
</ToggleButton>
</> </>
)} )}
@ -338,21 +326,32 @@ export const FeeManager = ({ selectedCoin, setFee, fee }) => {
{recommendedFeeDisplay && ( {recommendedFeeDisplay && (
<> <>
<Spacer height="15px" /> <Spacer height="15px" />
<Box sx={{ <Box
width: '100%', sx={{
display: 'flex', width: "100%",
justifyContent: 'center' display: "flex",
}}> justifyContent: "center",
}}
>
<Typography <Typography
sx={{ sx={{
color: "white", color: "white",
fontSize: "18px", fontSize: "18px",
}} }}
> >
<span style={{ <span
fontWeight: 'bold' style={{
}}> New fee:</span>{" "} fontWeight: "bold",
{calculateFeeFromRate(recommendedFeeDisplay, 300)} sats }}
>
{" "}
New fee:
</span>{" "}
{calculateFeeFromRate(
recommendedFeeDisplay,
300
)}{" "}
sats
</Typography> </Typography>
</Box> </Box>
<Spacer height="10px" /> <Spacer height="10px" />
@ -376,11 +375,10 @@ export const FeeManager = ({ selectedCoin, setFee, fee }) => {
> >
This recommended fee is derived from{" "} This recommended fee is derived from{" "}
{recommendedFeeDisplay} per kb, for a transaction that {recommendedFeeDisplay} per kb, for a transaction that
is approximately 300 kB in size. is approximately 300 bytes in size.
</Typography> </Typography>
</Box> </Box>
<Spacer height="10px" /> <Spacer height="10px" />
</> </>
)} )}
</Box> </Box>
@ -408,7 +406,7 @@ export const FeeManager = ({ selectedCoin, setFee, fee }) => {
<ButtonBase <ButtonBase
onClick={updateFee} onClick={updateFee}
disabled={(recommendedFee === 'custom' && !editFee)} disabled={recommendedFee === "custom" && !editFee}
sx={{ sx={{
minHeight: "42px", minHeight: "42px",
border: "1px solid gray", border: "1px solid gray",
@ -431,9 +429,14 @@ export const FeeManager = ({ selectedCoin, setFee, fee }) => {
<Typography>Update fee</Typography> <Typography>Update fee</Typography>
</ButtonBase> </ButtonBase>
{!hideRecommendations && feeTimestamp && ( {!hideRecommendations && feeTimestamp && (
<CustomLabel sx={{ <CustomLabel
textAlign: 'center' sx={{
}}>*Recommended fees last updated: {formatTimestampForum(feeTimestamp)}</CustomLabel> textAlign: "center",
}}
>
*Recommended fees last updated:{" "}
{formatTimestampForum(feeTimestamp)}
</CustomLabel>
)} )}
</CoinActionContainer> </CoinActionContainer>
</ReusableModal> </ReusableModal>

View File

@ -39,20 +39,34 @@ import { SetLeftFeature } from "ag-grid-community";
import { formatTimestampForum } from "../../utils/formatTime"; import { formatTimestampForum } from "../../utils/formatTime";
import { SelectRow } from "../header/Header"; import { SelectRow } from "../header/Header";
import { useAtom } from "jotai/react"; import { useAtom } from "jotai/react";
import { isEnabledCustomLockingFeeAtom, selectedFeePublisherAtom } from "../../global/state"; import {
isEnabledCustomLockingFeeAtom,
selectedFeePublisherAtom,
} from "../../global/state";
import { useRecommendedFees } from "../../hooks/useRecommendedFees";
export const Settings = () => { export const Settings = () => {
const saveDataLocal = useGlobal().persistentOperations.saveData const saveDataLocal = useGlobal().persistentOperations.saveData;
const getDataLocal = useGlobal().persistentOperations.getData const getDataLocal = useGlobal().persistentOperations.getData;
const [openModal, setOpenModal] = useState(false); const [openModal, setOpenModal] = useState(false);
const [lockingFee, setLockingFee] = useState(""); const [lockingFee, setLockingFee] = useState("");
const [recommendedFee, setRecommendedFee] = useState("medium_fee_per_kb");
const [selectedCoin, setSelectedCoin] = useState("LITECOIN");
const { hideRecommendations, recommendedFeeDisplay, coin } = useRecommendedFees({
selectedCoin,
recommendedFee
});
const [editLockingFee, setEditLockingFee] = useState(""); const [editLockingFee, setEditLockingFee] = useState("");
const [openAlert, setOpenAlert] = useState(false); const [openAlert, setOpenAlert] = useState(false);
const [info, setInfo] = useState<any>(null); const [info, setInfo] = useState<any>(null);
const [selectedCoin, setSelectedCoin] = useState("LTC"); const [selectedFeePublisher, setSelectedFeePublisher] = useAtom(
const [selectedFeePublisher, setSelectedFeePublisher] = useAtom(selectedFeePublisherAtom) selectedFeePublisherAtom
const [isEnabledCustomLockingFee, setIsEnabledCustomLockingFee] = useAtom(isEnabledCustomLockingFeeAtom) );
const [isEnabledCustomLockingFee, setIsEnabledCustomLockingFee] = useAtom(
isEnabledCustomLockingFeeAtom
);
const handleCloseAlert = ( const handleCloseAlert = (
event?: React.SyntheticEvent | Event, event?: React.SyntheticEvent | Event,
reason?: SnackbarCloseReason reason?: SnackbarCloseReason
@ -69,12 +83,14 @@ export const Settings = () => {
const typeRequest = "feekb"; const typeRequest = "feekb";
try { try {
const feeToSave = editLockingFee; let feeToSave = editLockingFee;
if (recommendedFee !== "custom") {
feeToSave = recommendedFeeDisplay
}
const response = await qortalRequestWithTimeout( const response = await qortalRequestWithTimeout(
{ {
action: "UPDATE_FOREIGN_FEE", action: "UPDATE_FOREIGN_FEE",
coin: selectedCoin, coin: coin,
type: typeRequest, type: typeRequest,
value: feeToSave, value: feeToSave,
}, },
@ -102,7 +118,16 @@ export const Settings = () => {
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setIsEnabledCustomLockingFee(event.target.checked); setIsEnabledCustomLockingFee(event.target.checked);
saveDataLocal('isEnabledCustomLockingFee', event.target.checked) saveDataLocal("isEnabledCustomLockingFee", event.target.checked);
};
const handleChangeRecommended = (
event: React.MouseEvent<HTMLElement>,
newAlignment: string
) => {
if (newAlignment) {
setRecommendedFee(newAlignment);
}
}; };
const establishUpdateFeeForm = useCallback(async (coin) => { const establishUpdateFeeForm = useCallback(async (coin) => {
@ -136,27 +161,33 @@ export const Settings = () => {
}, []); }, []);
useEffect(() => { useEffect(() => {
if(!openModal) return if (!openModal) return;
establishUpdateFeeForm(selectedCoin); establishUpdateFeeForm(coin);
}, [selectedCoin, establishUpdateFeeForm, openModal]); }, [coin, establishUpdateFeeForm, openModal]);
useEffect(() => { useEffect(() => {
const getSavedSelectedPublisher = async () => { const getSavedSelectedPublisher = async () => {
try { try {
const res = await getDataLocal('selectedFeePublisher') const res = await getDataLocal("selectedFeePublisher");
if (res) { if (res) {
setSelectedFeePublisher(res) setSelectedFeePublisher(res);
} }
const res2 = await getDataLocal('isEnabledCustomLockingFee') const res2 = await getDataLocal("isEnabledCustomLockingFee");
if (res2) { if (res2) {
setIsEnabledCustomLockingFee(res) setIsEnabledCustomLockingFee(res);
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error);
} }
};
getSavedSelectedPublisher();
}, []);
useEffect(() => {
if (hideRecommendations) {
setRecommendedFee("custom");
} }
getSavedSelectedPublisher() }, [hideRecommendations]);
}, [])
return ( return (
<> <>
@ -187,18 +218,33 @@ export const Settings = () => {
}} }}
open={openModal} open={openModal}
> >
<CoinActionContainer sx={{ <CoinActionContainer
border: '1px solid #3F3F3F', sx={{
borderRadius: '5px', border: "1px solid #3F3F3F",
padding: '5px' borderRadius: "5px",
}}> padding: "5px",
}}
>
<Typography>Locking fees</Typography> <Typography>Locking fees</Typography>
<FormControlLabel control={<Checkbox checked={isEnabledCustomLockingFee} onChange={handleChange} />} label="Enable custom locking fee" /> <FormControlLabel
control={
<Checkbox
checked={isEnabledCustomLockingFee}
onChange={handleChange}
/>
}
label="Enable custom locking fee"
/>
{isEnabledCustomLockingFee && ( {isEnabledCustomLockingFee && (
<CoinSelectRow sx={{ <>
gap: '20px' <CoinSelectRow
}}> sx={{
gap: "20px",
width: '100%',
justifyContent: 'center'
}}
>
<Select <Select
size="small" size="small"
value={selectedCoin} value={selectedCoin}
@ -208,25 +254,119 @@ export const Settings = () => {
setSelectedCoin(e.target.value); setSelectedCoin(e.target.value);
}} }}
> >
<MenuItem value={"LTC"}> <MenuItem value={"LITECOIN"}>
<SelectRow coin="LTC" /> <SelectRow coin="LTC" />
</MenuItem> </MenuItem>
<MenuItem value={"DOGE"}> <MenuItem value={"DOGECOIN"}>
<SelectRow coin="DOGE" /> <SelectRow coin="DOGE" />
</MenuItem> </MenuItem>
<MenuItem value={"BTC"}> <MenuItem value={"BITCOIN"}>
<SelectRow coin="BTC" /> <SelectRow coin="BTC" />
</MenuItem> </MenuItem>
<MenuItem value={"DGB"}> <MenuItem value={"DIGIBYTE"}>
<SelectRow coin="DGB" /> <SelectRow coin="DGB" />
</MenuItem> </MenuItem>
<MenuItem value={"RVN"}> <MenuItem value={"RAVENCOIN"}>
<SelectRow coin="RVN" /> <SelectRow coin="RVN" />
</MenuItem> </MenuItem>
<MenuItem value={"PIRATECHAIN"}>
<SelectRow coin="ARRR" />
</MenuItem>
</Select> </Select>
<Box>
</Box>
</CoinSelectRow>
<CoinActionRow>
<HeaderRow>
<Box
sx={{
width: "100%",
}}
>
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<CustomLabel
sx={{
fontSize: "16px",
}}
htmlFor="standard-adornment-name"
>
Recommended fee selection (in sats per kb)
</CustomLabel>
<Spacer height="10px" />
<ToggleButtonGroup
color="primary"
value={recommendedFee}
exclusive
onChange={handleChangeRecommended}
aria-label="Platform"
>
{!hideRecommendations && (
<>
<ToggleButton value="low_fee_per_kb">
Low
</ToggleButton>
<ToggleButton value="medium_fee_per_kb">
Medium
</ToggleButton>
<ToggleButton value="high_fee_per_kb">
High
</ToggleButton>
</>
)}
<ToggleButton value="custom">Custom</ToggleButton>
</ToggleButtonGroup>
</Box>
{recommendedFeeDisplay && (
<>
<Spacer height="15px" />
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
<Typography
sx={{
color: "white",
fontSize: "18px",
}}
>
<span
style={{
fontWeight: "bold",
}}
>
{" "}
New fee:
</span>{" "}
{recommendedFeeDisplay}{" "}
sats per kb
</Typography>
</Box>
<Spacer height="10px" />
</>
)}
</Box>
</HeaderRow>
</CoinActionRow>
{recommendedFee === "custom" && (
<CoinActionRow>
<HeaderRow>
<Box> <Box>
<CustomLabel htmlFor="standard-adornment-name"> <CustomLabel htmlFor="standard-adornment-name">
Locking fee for {selectedCoin} (sats per kb) Custom fee
</CustomLabel> </CustomLabel>
<Spacer height="5px" /> <Spacer height="5px" />
<CustomInput <CustomInput
@ -237,13 +377,16 @@ export const Settings = () => {
autoComplete="off" autoComplete="off"
/> />
</Box> </Box>
</CoinSelectRow> </HeaderRow>
</CoinActionRow>
)}
</>
)} )}
<ButtonBase <ButtonBase
onClick={updateLockingFee} onClick={updateLockingFee}
disabled={!editLockingFee} disabled={recommendedFee === "custom" && !editLockingFee}
sx={{ sx={{
minHeight: "42px", minHeight: "42px",
border: "1px solid gray", border: "1px solid gray",
@ -267,11 +410,13 @@ export const Settings = () => {
</ButtonBase> </ButtonBase>
</CoinActionContainer> </CoinActionContainer>
<Spacer height="20px" /> <Spacer height="20px" />
<CoinActionContainer sx={{ <CoinActionContainer
border: '1px solid #3F3F3F', sx={{
borderRadius: '5px', border: "1px solid #3F3F3F",
padding: '5px' borderRadius: "5px",
}}> padding: "5px",
}}
>
<Typography>Fee publisher</Typography> <Typography>Fee publisher</Typography>
<Select <Select
size="small" size="small"
@ -279,9 +424,8 @@ export const Settings = () => {
onChange={(e) => { onChange={(e) => {
if (e.target.value) { if (e.target.value) {
setSelectedFeePublisher(e.target.value); setSelectedFeePublisher(e.target.value);
saveDataLocal('selectedFeePublisher', e.target.value) saveDataLocal("selectedFeePublisher", e.target.value);
} }
}} }}
> >
<MenuItem value={"Foreign-Fee-Publisher"}> <MenuItem value={"Foreign-Fee-Publisher"}>

View File

@ -0,0 +1,76 @@
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'
import gameContext from '../contexts/gameContext';
import { QortalGetMetadata, usePublish } from 'qapp-core';
import { useAtom } from 'jotai/react';
import { selectedFeePublisherAtom } from '../global/state';
import { isValidFeeEstimate } from '../components/sell/FeeManager';
export const useRecommendedFees = ({selectedCoin, recommendedFee}) => {
const { getCoinLabel } = useContext(gameContext);
const [selectedFeePublisher, setSelectedFeePublisher] = useAtom(
selectedFeePublisherAtom
);
const [feeLocation, setFeeLocation] = useState<QortalGetMetadata>({
name: "",
identifier: "",
service: "JSON",
});
const { resource } = usePublish(3, "JSON", feeLocation);
const coin = useMemo(() => {
const coinLabel = getCoinLabel(selectedCoin);
if (typeof coinLabel !== "string") return null;
return coinLabel?.toLowerCase();
}, [selectedCoin, getCoinLabel]);
const getLatestFees = useCallback(async () => {
try {
const coinLabel = getCoinLabel(selectedCoin);
if (typeof coinLabel !== "string") return;
const coin = coinLabel?.toUpperCase();
const identifier = `coinInfo-${coin}`;
const res = await fetch(
`/arbitrary/resources/searchsimple?service=JSON&identifier=${identifier}&name=${selectedFeePublisher}&prefix=true&limit=1&reverse=true`
);
const data = await res.json();
if (data && data?.length > 0) {
setFeeLocation(data[0]);
}
} catch (error) {
console.error(error);
}
}, [selectedFeePublisher, selectedCoin]);
useEffect(() => {
getLatestFees();
}, [getLatestFees]);
const recommendedFeeData = useMemo(() => {
if (
!resource?.qortalMetadata?.identifier?.includes(`${coin.toUpperCase()}`)
)
return;
if (!resource?.data) return null;
const isValid = isValidFeeEstimate(resource.data);
if (!isValid) return null;
return resource.data;
}, [resource, coin]);
const recommendedFeeDisplay = useMemo(() => {
if (!recommendedFeeData) return null;
if (!recommendedFeeData) return null;
return recommendedFeeData[recommendedFee] || null;
}, [recommendedFeeData, recommendedFee]);
const hideRecommendations = useMemo(() => {
if (recommendedFeeData) return false;
return true;
}, [recommendedFeeData]);
return {
hideRecommendations,
recommendedFeeDisplay,
coin
}
}

View File

@ -0,0 +1,61 @@
import { useAtom } from 'jotai/react';
import React, { useCallback, useContext, useMemo } from 'react'
import { calculateRateFromFee } from '../components/sell/FeeManager';
import { isEnabledCustomLockingFeeAtom } from '../global/state';
import gameContext from '../contexts/gameContext';
export const useUpdateFee = ({setFee, selectedCoin}) => {
const [isEnabledCustomLockingFee, setIsEnabledCustomLockingFee] = useAtom(
isEnabledCustomLockingFeeAtom
);
const {
getCoinLabel,
} = useContext(gameContext);
const coin = useMemo(() => {
const coinLabel = getCoinLabel(selectedCoin);
if (typeof coinLabel !== "string") return null;
return coinLabel?.toLowerCase();
}, [selectedCoin, getCoinLabel]);
const updateFee = useCallback(async (suggestedFee ) => {
const typeRequest = "feerequired";
const typeRequestLocking = "feekb";
const feeToSave = +suggestedFee
const response = await qortalRequestWithTimeout(
{
action: "UPDATE_FOREIGN_FEE",
coin: coin,
type: typeRequest,
value: feeToSave,
},
1800000
);
if (!isEnabledCustomLockingFee) {
await qortalRequestWithTimeout(
{
action: "UPDATE_FOREIGN_FEE",
coin: coin,
type: typeRequestLocking,
value: calculateRateFromFee(feeToSave, 300),
},
1800000
);
}
if (response && !isNaN(+response)) {
setFee(response);
return response
} else throw new Error("Unable to update fee");
}, [coin, isEnabledCustomLockingFee, setFee]);
return updateFee
}

View File

@ -113,8 +113,8 @@ export const HomePage = () => {
</TextTableTitle> </TextTableTitle>
</Box> </Box>
<Spacer height="10px" /> <Spacer height="10px" />
<TradeOffers fee={fee} <TradeOffers setFee={setFee} fee={fee}
setFee={setFee} foreignCoinBalance={foreignCoinBalance} /> foreignCoinBalance={foreignCoinBalance} />
</div> </div>
<CreateSell show={mode === "sell"} qortAddress={userInfo?.address} /> <CreateSell show={mode === "sell"} qortAddress={userInfo?.address} />