manage fees

This commit is contained in:
PhilReact 2025-05-02 18:35:57 +03:00
parent 4bb3ca2685
commit c609fbb054
15 changed files with 1936 additions and 622 deletions

576
package-lock.json generated
View File

@ -18,10 +18,10 @@
"file-saver": "^2.0.5",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"react": "^18.2.0",
"react-copy-to-clipboard": "^5.1.0",
"qapp-core": "^1.0.22",
"react": "^19.1.0",
"react-countdown-circle-timer": "^3.2.1",
"react-dom": "^18.2.0",
"react-dom": "^19.1.0",
"react-ga4": "^2.1.0",
"react-loader-spinner": "^6.1.6",
"react-qr-code": "^2.0.15",
@ -33,8 +33,8 @@
},
"devDependencies": {
"@types/lodash": "^4.17.5",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@types/react": "^19.1.0",
"@types/react-dom": "^19.1.0",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"@vitejs/plugin-react": "^4.2.1",
@ -1791,15 +1791,15 @@
}
},
"node_modules/@emotion/babel-plugin": {
"version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
"integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==",
"version": "11.13.5",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
"integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
"dependencies": {
"@babel/helper-module-imports": "^7.16.7",
"@babel/runtime": "^7.18.3",
"@emotion/hash": "^0.9.1",
"@emotion/memoize": "^0.8.1",
"@emotion/serialize": "^1.1.2",
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/serialize": "^1.3.3",
"babel-plugin-macros": "^3.1.0",
"convert-source-map": "^1.5.0",
"escape-string-regexp": "^4.0.0",
@ -1808,6 +1808,11 @@
"stylis": "4.2.0"
}
},
"node_modules/@emotion/babel-plugin/node_modules/@emotion/memoize": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="
},
"node_modules/@emotion/babel-plugin/node_modules/convert-source-map": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
@ -1841,11 +1846,6 @@
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="
},
"node_modules/@emotion/cache/node_modules/@emotion/weak-memoize": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="
},
"node_modules/@emotion/hash": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
@ -1865,17 +1865,17 @@
"integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA=="
},
"node_modules/@emotion/react": {
"version": "11.11.4",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz",
"integrity": "sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==",
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.11.0",
"@emotion/cache": "^11.11.0",
"@emotion/serialize": "^1.1.3",
"@emotion/use-insertion-effect-with-fallbacks": "^1.0.1",
"@emotion/utils": "^1.2.1",
"@emotion/weak-memoize": "^0.3.1",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/cache": "^11.14.0",
"@emotion/serialize": "^1.3.3",
"@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
"@emotion/utils": "^1.4.2",
"@emotion/weak-memoize": "^0.4.0",
"hoist-non-react-statics": "^3.3.1"
},
"peerDependencies": {
@ -1915,16 +1915,16 @@
"integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg=="
},
"node_modules/@emotion/styled": {
"version": "11.11.5",
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.5.tgz",
"integrity": "sha512-/ZjjnaNKvuMPxcIiUkf/9SHoG4Q196DRl1w82hQ3WCsjo1IUR8uaGWrC6a87CrYAW0Kb/pK7hk8BnLgLRi9KoQ==",
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz",
"integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.11.0",
"@emotion/is-prop-valid": "^1.2.2",
"@emotion/serialize": "^1.1.4",
"@emotion/use-insertion-effect-with-fallbacks": "^1.0.1",
"@emotion/utils": "^1.2.1"
"@emotion/babel-plugin": "^11.13.5",
"@emotion/is-prop-valid": "^1.3.0",
"@emotion/serialize": "^1.3.3",
"@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
"@emotion/utils": "^1.4.2"
},
"peerDependencies": {
"@emotion/react": "^11.0.0-rc.0",
@ -1936,15 +1936,29 @@
}
}
},
"node_modules/@emotion/styled/node_modules/@emotion/is-prop-valid": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz",
"integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==",
"dependencies": {
"@emotion/memoize": "^0.9.0"
}
},
"node_modules/@emotion/styled/node_modules/@emotion/memoize": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="
},
"node_modules/@emotion/unitless": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
"integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ=="
"integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==",
"license": "MIT"
},
"node_modules/@emotion/use-insertion-effect-with-fallbacks": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz",
"integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz",
"integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==",
"peerDependencies": {
"react": ">=16.8.0"
}
@ -1955,9 +1969,9 @@
"integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA=="
},
"node_modules/@emotion/weak-memoize": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz",
"integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww=="
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.20.2",
@ -3109,6 +3123,31 @@
"string.prototype.matchall": "^4.0.6"
}
},
"node_modules/@tanstack/react-virtual": {
"version": "3.13.6",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.6.tgz",
"integrity": "sha512-WT7nWs8ximoQ0CDx/ngoFP7HbQF9Q2wQe4nh2NB+u2486eX3nZRE40P9g6ccCVq7ZfTSH5gFOuCoVH5DLNS/aA==",
"dependencies": {
"@tanstack/virtual-core": "3.13.6"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.13.6",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.6.tgz",
"integrity": "sha512-cnQUeWnhNP8tJ4WsGcYiX24Gjkc9ALstLbHcBj1t3E7EimN6n6kHH+DPV4PpDnuw00NApQp+ViojMj1GRdwYQg==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@ -3179,21 +3218,22 @@
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="
},
"node_modules/@types/react": {
"version": "18.2.79",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.79.tgz",
"integrity": "sha512-RwGAGXPl9kSXwdNTafkOEuFrTBD5SA2B3iEB96xi8+xu5ddUa/cpvyVCSNn+asgLCTHkb5ZxN8gbuibYJi4s1w==",
"version": "19.1.2",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz",
"integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==",
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-dom": {
"version": "18.2.25",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.25.tgz",
"integrity": "sha512-o/V48vf4MQh7juIKZU2QGDfli6p1+OOi5oXx36Hffpc9adsHeXjVp8rHuPkjd8VT8sOJ2Zp05HR7CdpGTIUFUA==",
"version": "19.1.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.3.tgz",
"integrity": "sha512-rJXC08OG0h3W6wDMFxQrZF00Kq6qQvw0djHRdzl3U5DnIERz0MRce3WVc7IS6JYBwtaP/DwYtRRjVlvivNveKg==",
"dev": true,
"dependencies": {
"@types/react": "*"
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.0.0"
}
},
"node_modules/@types/react-transition-group": {
@ -3210,6 +3250,11 @@
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
"dev": true
},
"node_modules/@types/seedrandom": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-3.0.8.tgz",
"integrity": "sha512-TY1eezMU2zH2ozQoAFAQFOPpvP15g+ZgSfTZt31AUUH/Rxtnz3H+A/Sv1Snw2/amp//omibc+AEkTaA8KUeOLQ=="
},
"node_modules/@types/semver": {
"version": "7.5.8",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
@ -3219,13 +3264,14 @@
"node_modules/@types/stylis": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz",
"integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw=="
"integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==",
"license": "MIT"
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"dev": true
"devOptional": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.7.0",
@ -3610,6 +3656,14 @@
"node": ">= 4.0.0"
}
},
"node_modules/attr-accept": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
"integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==",
"engines": {
"node": ">=4"
}
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@ -3703,6 +3757,33 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -3714,6 +3795,29 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/bloom-filters": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/bloom-filters/-/bloom-filters-3.0.4.tgz",
"integrity": "sha512-BdnPWo2OpYhlvuP2fRzJBdioMCkm7Zp0HCf8NJgF5Mbyqy7VQ/CnTiVWMMyq4EZCBHwj0Kq6098gW2/3RsZsrA==",
"dependencies": {
"@types/seedrandom": "^3.0.8",
"base64-arraybuffer": "^1.0.2",
"is-buffer": "^2.0.5",
"lodash": "^4.17.21",
"long": "^5.2.0",
"reflect-metadata": "^0.1.13",
"seedrandom": "^3.0.5",
"xxhashjs": "^0.2.2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/blueimp-canvas-to-blob": {
"version": "3.29.0",
"resolved": "https://registry.npmjs.org/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.29.0.tgz",
"integrity": "sha512-0pcSSGxC0QxT+yVkivxIqW0Y4VlO2XSDPofBAqoJ1qJxgH9eiUDLv50Rixij2cDuEfx4M6DpD9UGZpRhT5Q8qg=="
},
"node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@ -3766,6 +3870,29 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@ -3815,6 +3942,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
"integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@ -3933,6 +4061,15 @@
"node": ">=4.0.0"
}
},
"node_modules/compressorjs": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/compressorjs/-/compressorjs-1.2.1.tgz",
"integrity": "sha512-+geIjeRnPhQ+LLvvA7wxBQE5ddeLU7pJ3FsKFWirDw6veY3s9iLxAQEw7lXGHnhCJvBujEQWuNnGzZcvCvdkLQ==",
"dependencies": {
"blueimp-canvas-to-blob": "^3.29.0",
"is-blob": "^2.1.0"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -3945,14 +4082,6 @@
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true
},
"node_modules/copy-to-clipboard": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
"integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==",
"dependencies": {
"toggle-selection": "^1.0.6"
}
},
"node_modules/core-js-compat": {
"version": "3.38.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz",
@ -3995,6 +4124,11 @@
"node": ">= 8"
}
},
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
},
"node_modules/crypto-random-string": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
@ -4008,6 +4142,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
"integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
"license": "ISC",
"engines": {
"node": ">=4"
}
@ -4016,6 +4151,7 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
"integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
"license": "MIT",
"dependencies": {
"camelize": "^1.0.0",
"css-color-keywords": "^1.0.0",
@ -4027,6 +4163,11 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/cuint": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz",
"integrity": "sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw=="
},
"node_modules/data-view-buffer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz",
@ -4078,6 +4219,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="
},
"node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
@ -4179,11 +4325,20 @@
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/dompurify": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.5.tgz",
"integrity": "sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ==",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/ejs": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
@ -4778,6 +4933,22 @@
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
},
"node_modules/file-selector": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz",
"integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==",
"dependencies": {
"tslib": "^2.7.0"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/file-selector/node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"node_modules/filelist": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
@ -5110,6 +5281,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/goober": {
"version": "2.1.16",
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz",
"integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==",
"peerDependencies": {
"csstype": "^3.0.10"
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@ -5232,6 +5411,25 @@
"integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==",
"dev": true
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/ignore": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
@ -5344,6 +5542,17 @@
"node": ">=8"
}
},
"node_modules/is-blob": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-blob/-/is-blob-2.1.0.tgz",
"integrity": "sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw==",
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-boolean-object": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
@ -5360,6 +5569,28 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-buffer": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"engines": {
"node": ">=4"
}
},
"node_modules/is-builtin-module": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
@ -5901,6 +6132,11 @@
"integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==",
"dev": true
},
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@ -6208,9 +6444,10 @@
}
},
"node_modules/picocolors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
@ -6233,9 +6470,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.38",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
"version": "8.4.49",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
"funding": [
{
"type": "opencollective",
@ -6250,10 +6487,11 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.0",
"source-map-js": "^1.2.0"
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
@ -6262,7 +6500,8 @@
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"license": "MIT"
},
"node_modules/prelude-ls": {
"version": "1.2.1",
@ -6314,6 +6553,33 @@
"node": ">=6"
}
},
"node_modules/qapp-core": {
"version": "1.0.22",
"resolved": "https://registry.npmjs.org/qapp-core/-/qapp-core-1.0.22.tgz",
"integrity": "sha512-3q8Ebr9lpyDW7lTo91Rlak2EGIAXrU9DYhUBuLsYZMuRyI/bKoc0E4hHrrAo946eJ/5AxqNGmZDkYSJq5tOTZg==",
"license": "MIT",
"dependencies": {
"@tanstack/react-virtual": "^3.13.2",
"bloom-filters": "^3.0.4",
"buffer": "^6.0.3",
"compressorjs": "^1.2.1",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"dompurify": "^3.2.4",
"react-dropzone": "^14.3.8",
"react-hot-toast": "^2.5.2",
"react-intersection-observer": "^9.16.0",
"short-unique-id": "^5.2.0",
"zustand": "^4.3.2"
},
"peerDependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^7.0.1",
"@mui/material": "^7.0.1",
"react": "^19.0.0"
}
},
"node_modules/qr.js": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz",
@ -6349,28 +6615,14 @@
}
},
"node_modules/react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
"dependencies": {
"loose-envify": "^1.1.0"
},
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-copy-to-clipboard": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz",
"integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==",
"dependencies": {
"copy-to-clipboard": "^3.3.1",
"prop-types": "^15.8.1"
},
"peerDependencies": {
"react": "^15.3.0 || 16 || 17 || 18"
}
},
"node_modules/react-countdown-circle-timer": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/react-countdown-circle-timer/-/react-countdown-circle-timer-3.2.1.tgz",
@ -6380,15 +6632,31 @@
}
},
"node_modules/react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.0"
"scheduler": "^0.26.0"
},
"peerDependencies": {
"react": "^18.2.0"
"react": "^19.1.0"
}
},
"node_modules/react-dropzone": {
"version": "14.3.8",
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz",
"integrity": "sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==",
"dependencies": {
"attr-accept": "^2.2.4",
"file-selector": "^2.1.0",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">= 10.13"
},
"peerDependencies": {
"react": ">= 16.8 || 18.0.0"
}
},
"node_modules/react-ga4": {
@ -6396,6 +6664,36 @@
"resolved": "https://registry.npmjs.org/react-ga4/-/react-ga4-2.1.0.tgz",
"integrity": "sha512-ZKS7PGNFqqMd3PJ6+C2Jtz/o1iU9ggiy8Y8nUeksgVuvNISbmrQtJiZNvC/TjDsqD0QlU5Wkgs7i+w9+OjHhhQ=="
},
"node_modules/react-hot-toast": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.2.tgz",
"integrity": "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw==",
"dependencies": {
"csstype": "^3.1.3",
"goober": "^2.1.16"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
}
},
"node_modules/react-intersection-observer": {
"version": "9.16.0",
"resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz",
"integrity": "sha512-w9nJSEp+DrW9KmQmeWHQyfaP6b03v+TdXynaoA964Wxt7mdR3An11z4NNCQgL4gKSK7y1ver2Fq+JKH6CWEzUA==",
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
@ -6484,6 +6782,7 @@
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"license": "BSD-3-Clause",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
@ -6506,6 +6805,11 @@
"node": ">=8.10.0"
}
},
"node_modules/reflect-metadata": {
"version": "0.1.14",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz",
"integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A=="
},
"node_modules/regenerate": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@ -6782,12 +7086,15 @@
}
},
"node_modules/scheduler": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
"integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
"dependencies": {
"loose-envify": "^1.1.0"
}
"version": "0.26.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
"license": "MIT"
},
"node_modules/seedrandom": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="
},
"node_modules/semver": {
"version": "7.6.0",
@ -6866,7 +7173,8 @@
"node_modules/shallowequal": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==",
"license": "MIT"
},
"node_modules/shebang-command": {
"version": "2.0.0",
@ -6966,9 +7274,10 @@
}
},
"node_modules/source-map-js": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
@ -7122,16 +7431,17 @@
}
},
"node_modules/styled-components": {
"version": "6.1.13",
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.13.tgz",
"integrity": "sha512-M0+N2xSnAtwcVAQeFEsGWFFxXDftHUD7XrKla06QbpUMmbmtFBMMTcKWvFXtWxuD5qQkB8iU5gk6QASlx2ZRMw==",
"version": "6.1.17",
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.17.tgz",
"integrity": "sha512-97D7DwWanI7nN24v0D4SvbfjLE9656umNSJZkBkDIWL37aZqG/wRQ+Y9pWtXyBIM/NSfcBzHLErEsqHmJNSVUg==",
"license": "MIT",
"dependencies": {
"@emotion/is-prop-valid": "1.2.2",
"@emotion/unitless": "0.8.1",
"@types/stylis": "4.2.5",
"css-to-react-native": "3.2.0",
"csstype": "3.1.3",
"postcss": "8.4.38",
"postcss": "8.4.49",
"shallowequal": "1.1.0",
"stylis": "4.3.2",
"tslib": "2.6.2"
@ -7151,7 +7461,8 @@
"node_modules/styled-components/node_modules/stylis": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz",
"integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg=="
"integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==",
"license": "MIT"
},
"node_modules/stylis": {
"version": "4.2.0",
@ -7301,11 +7612,6 @@
"node": ">=8.0"
}
},
"node_modules/toggle-selection": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="
},
"node_modules/tr46": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz",
@ -7330,7 +7636,8 @@
"node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
"license": "0BSD"
},
"node_modules/type-check": {
"version": "0.4.0",
@ -7567,6 +7874,14 @@
"punycode": "^2.1.0"
}
},
"node_modules/use-sync-external-store": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/vite": {
"version": "5.2.9",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.9.tgz",
@ -8087,6 +8402,14 @@
"node": ">=0.4.0"
}
},
"node_modules/xxhashjs": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/xxhashjs/-/xxhashjs-0.2.2.tgz",
"integrity": "sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==",
"dependencies": {
"cuint": "^0.2.2"
}
},
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@ -8112,6 +8435,33 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zustand": {
"version": "4.5.6",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz",
"integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==",
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
}
}
}

View File

@ -20,10 +20,10 @@
"file-saver": "^2.0.5",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"react": "^18.2.0",
"react-copy-to-clipboard": "^5.1.0",
"qapp-core": "^1.0.22",
"react": "^19.1.0",
"react-countdown-circle-timer": "^3.2.1",
"react-dom": "^18.2.0",
"react-dom": "^19.1.0",
"react-ga4": "^2.1.0",
"react-loader-spinner": "^6.1.6",
"react-qr-code": "^2.0.15",
@ -35,8 +35,8 @@
},
"devDependencies": {
"@types/lodash": "^4.17.5",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@types/react": "^19.1.0",
"@types/react-dom": "^19.1.0",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"@vitejs/plugin-react": "^4.2.1",
@ -46,5 +46,11 @@
"typescript": "^5.4.5",
"vite": "^5.2.0",
"vite-plugin-pwa": "^0.20.5"
},
"overrides": {
"react-loader-spinner": {
"react": "^18 || ^19",
"react-dom": "^18 || ^19"
}
}
}

View File

@ -90,7 +90,7 @@
.ag-theme-alpine-dark .ag-row-odd,
.ag-theme-alpine-dark .ag-row-even {
width: 100%;
background-color: #292929 !important; /* Replace with your desired color */
background-color: #292929; /* Replace with your desired color */
}
.ag-theme-alpine-dark {
@ -144,3 +144,4 @@
background-clip: content-box;
border: 4px solid transparent;
}

View File

@ -1,7 +1,6 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import ReactGA from "react-ga4";
import "./App.css";
import socketService from "./services/socketService";
import GameContext, {
IContextProps,
UserNameAvatar,
@ -22,9 +21,7 @@ import axios from "axios";
import { executeEvent } from "./utils/events";
import { useIndexedDBContext } from "./contexts/indexedDBContext";
import { useGetOngoingTransactions } from "./components/DbComponents/OngoingTransactions";
import { GlobalProvider } from "qapp-core";
export async function sendRequestToExtension(
requestType: string,
@ -66,8 +63,6 @@ export async function sendRequestToExtension(
});
}
function App() {
const [userInfo, setUserInfo] = useState<any>(null);
const [qortBalance, setQortBalance] = useState<any>(null);
@ -75,17 +70,25 @@ function App() {
const [selectedCoin, setSelectedCoin] = useState("LITECOIN");
const foreignCoinBalance = useMemo(() => {
if(balances[selectedCoin] === 0) return 0
return balances[selectedCoin] || null
}, [balances, selectedCoin])
if (balances[selectedCoin] === 0) return 0;
return balances[selectedCoin] || null;
}, [balances, selectedCoin]);
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
const [OAuthLoading, setOAuthLoading] = useState<boolean>(false);
const db = useIndexedDBContext();
const [isUsingGateway, setIsUsingGateway] = useState(null)
const [isUsingGateway, setIsUsingGateway] = useState(null);
const [isSocketUp, setIsSocketUp] = useState<boolean>(false);
// const [onGoingTrades, setOngoingTrades] = useState([])
const {onGoingTrades, fetchOngoingTransactions, updateTransactionInDB, deleteTemporarySellOrder, updateTemporaryFailedTradeBots, fetchTemporarySellOrders, sellOrders} = useGetOngoingTransactions({qortAddress: userInfo?.address})
const {
onGoingTrades,
fetchOngoingTransactions,
updateTransactionInDB,
deleteTemporarySellOrder,
updateTemporaryFailedTradeBots,
fetchTemporarySellOrders,
sellOrders,
} = useGetOngoingTransactions({ qortAddress: userInfo?.address });
const [userNameAvatar, setUserNameAvatar] = useState<
Record<string, UserNameAvatar>
>({});
@ -104,24 +107,20 @@ function App() {
const getIsUsingGateway = async () => {
try {
const res = await qortalRequest({
action: "IS_USING_PUBLIC_NODE"
})
setIsUsingGateway(res)
} catch (error) {
}
}
action: "IS_USING_PUBLIC_NODE",
});
setIsUsingGateway(res);
} catch (error) {}
};
useEffect(() => {
getIsUsingGateway()
}, [])
getIsUsingGateway();
}, []);
const resetNotification = () => {
setNotification({ alertType: "", msg: "" });
};
const userContextValue: UserContextProps = {
avatar,
setAvatar,
@ -164,53 +163,44 @@ function App() {
askForAccountInformation();
}, [askForAccountInformation]);
const getQortBalance = async () => {
const balanceUrl: string = `/addresses/balance/${userInfo?.address}`;
const balanceResponse = await axios(balanceUrl);
setQortBalance(balanceResponse.data?.value)
}
setQortBalance(balanceResponse.data?.value);
};
const getLTCBalance = async (coin) => {
try {
const response = await qortalRequest({
action: "GET_WALLET_BALANCE",
coin: getCoinLabel(coin)
coin: getCoinLabel(coin),
});
if (!response?.error) {
setBalances((prev) => {
return {
...prev,
[coin]: +response
}
})
[coin]: +response,
};
});
}
} catch (error) {
//
}
}
};
useEffect(() => {
if(!userInfo?.address || !selectedCoin) return
if (!userInfo?.address || !selectedCoin) return;
const intervalGetTradeInfo = setInterval(() => {
fetchOngoingTransactions()
getLTCBalance(selectedCoin)
getQortBalance()
}, 150000)
getLTCBalance(selectedCoin)
getQortBalance()
fetchOngoingTransactions();
getLTCBalance(selectedCoin);
getQortBalance();
}, 150000);
getLTCBalance(selectedCoin);
getQortBalance();
return () => {
clearInterval(intervalGetTradeInfo)
}
}, [userInfo?.address, isAuthenticated, selectedCoin])
clearInterval(intervalGetTradeInfo);
};
}, [userInfo?.address, isAuthenticated, selectedCoin]);
const handleMessage = async (event: any) => {
if (event.data.type === "LOGOUT") {
@ -218,16 +208,14 @@ function App() {
setUserInfo(null);
setAvatar("");
setIsAuthenticated(false);
setQortBalance(null)
setQortBalance(null);
localStorage.setItem("token", "");
} else if (event.data.type === "RESPONSE_FOR_TRADES") {
const response = event.data.payload
if (response?.extra?.atAddresses
) {
const response = event.data.payload;
if (response?.extra?.atAddresses) {
try {
const status = response.callResponse === true ? 'trade-ongoing' : 'trade-failed'
const status =
response.callResponse === true ? "trade-ongoing" : "trade-failed";
const token = localStorage.getItem("token");
// Prepare transaction data
const transactionData = {
@ -239,10 +227,10 @@ function App() {
// Update transactions in IndexedDB
const result = await updateTransactionInDB(transactionData);
fetchOngoingTransactions()
executeEvent("execute-get-new-block-trades", {})
fetchOngoingTransactions();
executeEvent("execute-get-new-block-trades", {});
} catch (error) {
console.log({error})
console.log({ error });
}
}
}
@ -256,36 +244,33 @@ function App() {
};
}, [userInfo?.address]);
const getCoinLabel = useCallback((coin?: string)=> {
const getCoinLabel = useCallback(
(coin?: string) => {
switch (coin || selectedCoin) {
case "LITECOIN": {
return 'LTC'
return "LTC";
}
case "DOGECOIN": {
return 'DOGE'
return "DOGE";
}
case "BITCOIN": {
return 'BTC'
return "BTC";
}
case "DIGIBYTE": {
return 'DGB'
return "DGB";
}
case "RAVENCOIN": {
return 'RVN'
return "RVN";
}
case "PIRATECHAIN": {
return 'ARRR'
return "ARRR";
}
default:
return null
return null;
}
}, [selectedCoin])
},
[selectedCoin]
);
const gameContextValue: IContextProps = {
userInfo,
@ -302,11 +287,33 @@ function App() {
setOAuthLoading,
updateTransactionInDB,
sellOrders,
deleteTemporarySellOrder, updateTemporaryFailedTradeBots, fetchTemporarySellOrders, isUsingGateway, selectedCoin, setSelectedCoin, getCoinLabel
deleteTemporarySellOrder,
updateTemporaryFailedTradeBots,
fetchTemporarySellOrders,
isUsingGateway,
selectedCoin,
setSelectedCoin,
getCoinLabel,
};
return (
<GlobalProvider
config={{
auth: {
balanceSetting: {
interval: 180000,
onlyOnMount: false,
},
authenticateOnMount: false,
userAccountInfo: {
address: userInfo?.address,
publicKey: userInfo?.publicKey,
},
},
appName: "q-trade",
publicSalt: "SwQR6+uWzwm9Cqw1vqpR8zPKIThqdc5X7CUhWKn+rfc=",
}}
>
<NotificationContext.Provider value={notificationContextValue}>
<LoadingContext.Provider value={loadingContextValue}>
<UserContext.Provider value={userContextValue}>
@ -314,19 +321,14 @@ function App() {
<Notification />
<ThemeProvider theme={darkTheme}>
<Routes>
<Route
path="/"
element={
<HomePage
/>
}
/>
<Route path="/" element={<HomePage />} />
</Routes>
</ThemeProvider>
</GameContext.Provider>
</UserContext.Provider>
</LoadingContext.Provider>
</NotificationContext.Provider>
</GlobalProvider>
);
}

View File

@ -5,7 +5,7 @@ import { useIndexedDBContext } from "../../contexts/indexedDBContext";
const fetchTradeInfo = async (qortalAtAddress) => {
const checkIfOfferingRes = await fetch(`/crosschain/trade/${qortalAtAddress}`)
const checkIfOfferingRes = await fetch(`http://devnet-nodes.qortal.link:11112/crosschain/trade/${qortalAtAddress}`)
const data = await checkIfOfferingRes.json()
return data
};

View File

@ -24,7 +24,6 @@ export const BuyContainer = styled(Box)(({ theme }) => ({
justifyContent: "space-between",
alignItems: "center",
bottom: "0px",
height: "100px",
padding: "18px 14px 12px 14px",
background: "#323336",
zIndex: 3,

View File

@ -10,12 +10,13 @@ import { AgGridReact } from "ag-grid-react";
import {
ColDef,
RowClassParams,
RowNode,
RowStyle,
SizeColumnsToContentStrategy,
} from "ag-grid-community";
import "ag-grid-community/styles/ag-grid.css";
import "ag-grid-community/styles/ag-theme-alpine.css";
import InfoOutlineIcon from '@mui/icons-material/InfoOutline';
import InfoOutlineIcon from "@mui/icons-material/InfoOutline";
import {
Alert,
Box,
@ -26,6 +27,7 @@ import {
DialogContent,
DialogContentText,
DialogTitle,
FormControlLabel,
IconButton,
Snackbar,
SnackbarCloseReason,
@ -48,9 +50,8 @@ import {
MainContainer,
} from "./Table-styles";
export const baseLocalHost = window.location.host;
// export const baseLocalHost = "127.0.0.1:12391";
// export const baseLocalHost = window.location.host;
export const baseLocalHost = "devnet-nodes.qortal.link:11111";
import CloseIcon from "@mui/icons-material/Close";
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
@ -79,8 +80,12 @@ export const autoSizeStrategy: SizeColumnsToContentStrategy = {
type: "fitCellContents",
};
export const TradeOffers: React.FC<any> = ({ foreignCoinBalance }: any) => {
export const TradeOffers: React.FC<any> = ({
foreignCoinBalance,
fee,
}: any) => {
const [offers, setOffers] = useState<any[]>([]);
const [signedUnlockingFees, setSignedUnlockingFees] = useState(null);
const [qortalNames, setQortalNames] = useState({});
const {
fetchOngoingTransactions,
@ -90,6 +95,7 @@ export const TradeOffers: React.FC<any> = ({ foreignCoinBalance }: any) => {
getCoinLabel,
selectedCoin,
} = useContext(gameContext);
const isRemoveOrdersWithoutUnlockingFees = useRef(false);
const listOfOngoingTradesAts = useMemo(() => {
return (
onGoingTrades
@ -105,6 +111,14 @@ export const TradeOffers: React.FC<any> = ({ foreignCoinBalance }: any) => {
message: messageInfo,
} = useModal();
const {
isShow: isShowTradesUnknownFee,
onCancel: onCancelTradesUnknownFee,
onOk: onOkTradesUnknownFee,
show: showTradesUnknownFee,
message: messageTradesUnknownFee,
} = useModal();
const offersWithoutOngoing = useMemo(() => {
return offers.filter(
(item) => !listOfOngoingTradesAts.includes(item.qortalAtAddress)
@ -122,13 +136,39 @@ export const TradeOffers: React.FC<any> = ({ foreignCoinBalance }: any) => {
const offeringTrades = useRef<any[]>([]);
const blockedTradesList = useRef([]);
const gridRef = useRef<any>(null);
const [openShowOfferDetails, setOpenShowOfferDetails] = useState(null)
const [openShowOfferDetails, setOpenShowOfferDetails] = useState(null);
const [open, setOpen] = useState(false);
const [info, setInfo] = useState<any>(null);
const BuyButton = () => {
return <BuyOrderBtn onClick={buyOrder}>BUY</BuyOrderBtn>;
};
const intervalGetSignedUnlockingFees = useRef<number | null>(null);
const signedUnlockingFeesRef = useRef(signedUnlockingFees);
const feeRef = useRef(fee);
const knownFees = useMemo(() => {
const offersWithKnownFees = [];
selectedOffers.forEach((offer) => {
const feeEntry = signedUnlockingFees.find(
(item) => item?.atAddress === offer.qortalAtAddress
);
if (feeEntry && typeof feeEntry.fee === "number") {
offersWithKnownFees.push({ ...offer, fee: feeEntry.fee });
}
});
const totalKnownFees = offersWithKnownFees.reduce((sum, offer) => {
return sum + (offer.fee || 0);
}, 0);
const feeInLtc = totalKnownFees / 1e8;
return +feeInLtc.toFixed(8);
}, [selectedOffers, signedUnlockingFees]);
console.log("knownFees", knownFees);
const defaultColDef = {
resizable: true, // Make columns resizable by default
sortable: true, // Make columns sortable by default
@ -176,8 +216,6 @@ export const TradeOffers: React.FC<any> = ({ foreignCoinBalance }: any) => {
}
};
const columnDefs: ColDef[] = useMemo(() => {
return [
{
@ -190,10 +228,23 @@ export const TradeOffers: React.FC<any> = ({ foreignCoinBalance }: any) => {
resizable: false,
suppressRowClickSelection: true,
cellRenderer: (params) =>
<SelectWithInfoCell {...params} selectTradeForDetails={()=> {
setOpenShowOfferDetails(params?.node?.data)
}} />,
cellRenderer: (params) => (
<SelectWithInfoCell
{...params}
selectTradeForDetails={() => {
const hasSignedFee = signedUnlockingFees?.find(
(item) =>
item?.atAddress === params?.node?.data?.qortalAtAddress
);
let fee = null;
if (hasSignedFee) {
fee = hasSignedFee.fee;
}
setOpenShowOfferDetails({ ...(params?.node?.data || {}), fee });
}}
/>
),
// suppressRowClickSelection: true, // prevent whole row selection on click
},
{
@ -220,6 +271,22 @@ export const TradeOffers: React.FC<any> = ({ foreignCoinBalance }: any) => {
minWidth: 150, // Ensure it doesn't shrink too much
resizable: true,
},
{
headerName: `Unlocking fee`,
flex: 1, // Flex makes this column responsive
minWidth: 150, // Ensure it doesn't shrink too much
resizable: true,
valueGetter: (params) => {
if (params?.data?.qortalAtAddress) {
console.log("22signedUnlockingFees", signedUnlockingFees);
const hasSignedFee = signedUnlockingFees?.find(
(item) => item?.atAddress === params.data.qortalAtAddress
);
if (!hasSignedFee) return "Unknown";
return hasSignedFee.fee;
} else return "Unknown";
},
},
{
headerName: "Seller",
field: "qortalCreator",
@ -241,7 +308,7 @@ export const TradeOffers: React.FC<any> = ({ foreignCoinBalance }: any) => {
},
},
];
}, [qortalNames, getCoinLabel]);
}, [qortalNames, getCoinLabel, signedUnlockingFees]);
// const onRowClicked = (event: any) => {
// if(listOfOngoingTradesAts.includes(event.data.qortalAtAddress)) return
@ -256,7 +323,7 @@ export const TradeOffers: React.FC<any> = ({ foreignCoinBalance }: any) => {
const getNewBlockedTrades = async () => {
const unconfirmedTransactionsList = async () => {
const unconfirmedTransactionslUrl = `/transactions/unconfirmed?txType=MESSAGE&limit=0&reverse=true`;
const unconfirmedTransactionslUrl = `http://devnet-nodes.qortal.link:11112/transactions/unconfirmed?txType=MESSAGE&limit=0&reverse=true`;
var addBlockedTrades = JSON.parse(
localStorage.getItem("failedTrades") || "[]"
@ -468,7 +535,7 @@ export const TradeOffers: React.FC<any> = ({ foreignCoinBalance }: any) => {
};
useEffect(() => {
if(isUsingGateway === null) return
if (isUsingGateway === null) return;
blockedTradesList.current = JSON.parse(
localStorage.getItem("failedTrades") || "[]"
);
@ -489,8 +556,36 @@ export const TradeOffers: React.FC<any> = ({ foreignCoinBalance }: any) => {
};
}, [isUsingGateway]);
const getSignedUnlockingFees = useCallback(async () => {
try {
const response = await fetch(
`http://devnet-nodes.qortal.link:11112/crosschain/signedfees`
);
const data = await response.json();
if (data && Array.isArray(data)) {
setSignedUnlockingFees(data);
}
} catch (error) {
console.error(error);
}
}, []);
console.log("signed", signedUnlockingFees);
useEffect(() => {
if(isUsingGateway === null) return
getSignedUnlockingFees();
intervalGetSignedUnlockingFees.current = setInterval(() => {
getSignedUnlockingFees();
}, 150000);
return () => {
if (intervalGetSignedUnlockingFees.current) {
clearInterval(intervalGetSignedUnlockingFees.current);
}
};
}, [getSignedUnlockingFees]);
useEffect(() => {
if (isUsingGateway === null) return;
if (selectedCoin === null) return;
restartTradeOffers();
setTimeout(() => {
@ -504,13 +599,17 @@ export const TradeOffers: React.FC<any> = ({ foreignCoinBalance }: any) => {
}, [isUsingGateway, selectedCoin]);
const selectedTotalLTC = useMemo(() => {
return selectedOffers.reduce((acc: number, curr: any) => {
const total = selectedOffers.reduce((acc: number, curr: any) => {
return acc + (+curr.foreignAmount || 0); // Ensure qortAmount is defined
}, 0);
}, [selectedOffers]);
const totalWithKnownFees = +total + +knownFees;
return totalWithKnownFees;
}, [selectedOffers, knownFees]);
const buyOrder = async () => {
try {
isRemoveOrdersWithoutUnlockingFees.current = false;
if (+foreignCoinBalance < +selectedTotalLTC.toFixed(4)) {
setOpen(true);
setInfo({
@ -521,6 +620,36 @@ export const TradeOffers: React.FC<any> = ({ foreignCoinBalance }: any) => {
}
if (selectedOffers?.length < 1) return;
let offersWithKnownFees = [];
const offersWithUnknownFees = [];
selectedOffers.forEach((offer) => {
const feeEntry = signedUnlockingFees.find(
(item) => item?.atAddress === offer.qortalAtAddress
);
if (feeEntry && typeof feeEntry.fee === "number") {
offersWithKnownFees.push({ ...offer, fee: feeEntry.fee });
} else {
offersWithUnknownFees.push(offer);
}
});
console.log("offersWithKnownFees", offersWithKnownFees);
console.log("offersWithUnknownFees", offersWithUnknownFees);
if (offersWithUnknownFees?.length > 0) {
await showTradesUnknownFee({
message: "",
});
if (!isRemoveOrdersWithoutUnlockingFees.current) {
offersWithKnownFees = [
...offersWithKnownFees,
...offersWithUnknownFees,
];
}
}
console.log("offersWithKnownFees", offersWithKnownFees);
setIsShowBuyInProgress({ status: "buying" });
@ -529,7 +658,7 @@ export const TradeOffers: React.FC<any> = ({ foreignCoinBalance }: any) => {
// type: 'info',
// message: "Attempting to submit buy order. Please wait..."
// })
const listOfATs = selectedOffers;
const listOfATs = offersWithKnownFees;
const response = await qortalRequestWithTimeout(
{
action: "CREATE_TRADE_BUY_ORDER",
@ -609,6 +738,18 @@ export const TradeOffers: React.FC<any> = ({ foreignCoinBalance }: any) => {
if (params.data.qortalAtAddress === selectedOffer?.qortalAtAddress) {
return { background: "#6D94F533" };
}
const hasSignedFee = signedUnlockingFees?.find(
(item) => item?.atAddress === params.data.qortalAtAddress
);
console.log("hasSignedFee", hasSignedFee);
if (hasSignedFee) {
console.log("fee2fee", fee);
if (fee && hasSignedFee?.fee > fee) {
return { backgroundColor: "#ff6347" };
}
}
return undefined;
};
// const onGridReady = (params) => {
@ -639,9 +780,10 @@ export const TradeOffers: React.FC<any> = ({ foreignCoinBalance }: any) => {
}, []);
const selectedTotalQORT = useMemo(() => {
return selectedOffers.reduce((acc: number, curr: any) => {
const total = selectedOffers.reduce((acc: number, curr: any) => {
return acc + (+curr.qortAmount || 0); // Ensure qortAmount is defined
}, 0);
return total;
}, [selectedOffers]);
const onGridReady = useCallback((params: any) => {
@ -664,6 +806,32 @@ export const TradeOffers: React.FC<any> = ({ foreignCoinBalance }: any) => {
setInfo(null);
};
useEffect(() => {
signedUnlockingFeesRef.current = signedUnlockingFees;
feeRef.current = fee;
if (gridRef.current?.api) {
console.log("Total rows:", gridRef.current.api.getDisplayedRowCount());
gridRef.current.api.forEachNode((rowNode: RowNode) => {
const qortalAtAddress = rowNode.data?.qortalAtAddress;
const hasSignedFee = signedUnlockingFeesRef.current?.find(
(item) => item?.atAddress === qortalAtAddress
);
const isSelectable =
!hasSignedFee || hasSignedFee.fee <= feeRef.current;
rowNode.setRowSelectable(isSelectable); // ✅ apply logic per row
});
// Optional: refresh selection/checkbox visuals
gridRef.current.api.refreshCells({ force: true });
}
}, [signedUnlockingFees, fee]);
console.log("signedUnlockingFees", signedUnlockingFees, fee);
if (!signedUnlockingFees || !fee) return null;
return (
<MainContainer>
<div
@ -688,6 +856,23 @@ export const TradeOffers: React.FC<any> = ({ foreignCoinBalance }: any) => {
onGridReady={onGridReady}
// domLayout='autoHeight'
getRowId={(params) => params.data.qortalAtAddress} // Ensure rows have unique IDs
gridOptions={{
isRowSelectable: (params) => {
let selectable = true;
const hasSignedFee = signedUnlockingFeesRef.current?.find(
(item) => item?.atAddress === params.data.qortalAtAddress
);
if (!hasSignedFee) selectable = true;
console.log(
"fee",
feeRef.current,
signedUnlockingFeesRef.current
);
if (hasSignedFee && hasSignedFee?.fee > feeRef.current)
selectable = false;
return selectable;
},
}}
/>
{/* {selectedOffer && (
<Button onClick={buyOrder}>Buy</Button>
@ -696,7 +881,7 @@ export const TradeOffers: React.FC<any> = ({ foreignCoinBalance }: any) => {
</div>
<div
style={{
height: "120px",
height: "150px",
}}
/>
<BuyContainer>
@ -716,7 +901,7 @@ export const TradeOffers: React.FC<any> = ({ foreignCoinBalance }: any) => {
width: "calc(100% - 75px)",
}}
>
{selectedTotalQORT?.toFixed(3)} QORT
{selectedTotalQORT?.toFixed(8)} QORT
</Typography>
<Box
sx={{
@ -729,15 +914,17 @@ export const TradeOffers: React.FC<any> = ({ foreignCoinBalance }: any) => {
<Typography
sx={{
fontSize: "16px",
color: selectedTotalLTC > foreignCoinBalance ? "red" : "white",
backgroundColor:
selectedTotalLTC > foreignCoinBalance ? "red" : "unset",
color: "white",
}}
>
<span>{selectedTotalLTC?.toFixed(4)}</span>{" "}
<span>{selectedTotalLTC?.toFixed(8)}</span>{" "}
<span
style={{
marginLeft: "auto",
}}
>{`${getCoinLabel()} `}</span>
>{`${getCoinLabel()} (with known fees)`}</span>
</Typography>
</Box>
<Typography
@ -746,7 +933,7 @@ export const TradeOffers: React.FC<any> = ({ foreignCoinBalance }: any) => {
color: "white",
}}
>
<span>{foreignCoinBalance?.toFixed(4)}</span>{" "}
<span>{foreignCoinBalance?.toFixed(8)}</span>{" "}
<span
style={{
marginLeft: "auto",
@ -814,6 +1001,81 @@ export const TradeOffers: React.FC<any> = ({ foreignCoinBalance }: any) => {
</DialogActions>
</Dialog>
)}
{isShowTradesUnknownFee && (
<Dialog
open={isShowTradesUnknownFee}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Warning</DialogTitle>
<DialogContent>
<DialogContentText
id="alert-dialog-description"
sx={{ color: "white" }}
>
Some of your buy orders have unknown unlocking fees.
</DialogContentText>
<Spacer height="20px" />
<DialogContentText
id="alert-dialog-description"
sx={{ color: "white" }}
>
You may proceed with your purchases without removing these orders,
but there is a higher risk that the trades may not go through. In
such cases, you will be refunded.
</DialogContentText>
<Spacer height="20px" />
<FormControlLabel
sx={{
margin: 0,
}}
control={
<Checkbox
onChange={(e) =>
(isRemoveOrdersWithoutUnlockingFees.current =
e.target.checked)
}
edge="start"
tabIndex={-1}
disableRipple
sx={{
"&.Mui-checked": {
color: "white",
},
"& .MuiSvgIcon-root": {
color: "white",
},
}}
/>
}
label={
<Box sx={{ display: "flex", alignItems: "center" }}>
<Typography sx={{ fontSize: "14px" }}>
Remove orders with unknown unlocking fees
</Typography>
</Box>
}
/>
</DialogContent>
<DialogActions>
<Button
variant="contained"
onClick={onCancelTradesUnknownFee}
autoFocus
>
Close
</Button>
<Button
variant="contained"
onClick={onOkTradesUnknownFee}
autoFocus
>
Continue
</Button>
</DialogActions>
</Dialog>
)}
{isShowBuyInProgress && (
<Dialog
open={isShowBuyInProgress}
@ -965,9 +1227,9 @@ export const TradeOffers: React.FC<any> = ({ foreignCoinBalance }: any) => {
}}
>
<Typography variant="subtitle1">
Buy {openShowOfferDetails?.qortAmount} QORT @ {openShowOfferDetails?.foreignAmount} {getCoinLabel()}
Buy {openShowOfferDetails?.qortAmount} QORT @{" "}
{openShowOfferDetails?.foreignAmount} {getCoinLabel()}
</Typography>
</DialogTitle>
<IconButton
aria-label="close"
@ -977,11 +1239,62 @@ export const TradeOffers: React.FC<any> = ({ foreignCoinBalance }: any) => {
<CloseIcon />
</IconButton>
<DialogContent dividers sx={{ borderColor: "#333" }}>
<TradeRow enableSlice enableCopy label="Seller" value={openShowOfferDetails?.qortalCreator} extra={qortalNames[openShowOfferDetails?.qortalCreator]} />
<TradeRow label="Amount" value={`${openShowOfferDetails?.qortAmount} QORT`} />
<TradeRow label="Total" value={`${openShowOfferDetails?.foreignAmount} ${getCoinLabel()}`} />
<TradeRow label="Price" value={`${+openShowOfferDetails?.foreignAmount / +openShowOfferDetails?.qortAmount } ${getCoinLabel()}/QORT`} />
<TradeRow enableSlice enableCopy label="AT Address" value={openShowOfferDetails?.qortalAtAddress} />
{fee &&
openShowOfferDetails?.fee &&
+fee < +openShowOfferDetails?.fee && (
<Box
sx={{
background: "red",
padding: "10px",
borderRadius: "5px",
}}
>
<Typography sx={{ color: "white" }}>
The unlocking fee on this node is lower than the amount
required for this order.
</Typography>
<Spacer height="10px" />
<Typography sx={{ color: "white" }}>
If you're using your own node, you can change the fee by
clicking the "Fee" button next to the coin selector.
</Typography>
</Box>
)}
<TradeRow
enableSlice
enableCopy
label="Seller"
value={openShowOfferDetails?.qortalCreator}
extra={qortalNames[openShowOfferDetails?.qortalCreator]}
/>
<TradeRow
label="Amount"
value={`${openShowOfferDetails?.qortAmount} QORT`}
/>
<TradeRow
label="Total"
value={`${openShowOfferDetails?.foreignAmount} ${getCoinLabel()}`}
/>
<TradeRow
label="Price"
value={`${
+openShowOfferDetails?.foreignAmount /
+openShowOfferDetails?.qortAmount
} ${getCoinLabel()}/QORT`}
/>
{openShowOfferDetails?.fee && (
<TradeRow
label="Unlocking fee"
value={`${openShowOfferDetails?.fee} sats`}
/>
)}
<TradeRow
enableSlice
enableCopy
label="AT Address"
value={openShowOfferDetails?.qortalAtAddress}
/>
</DialogContent>
<DialogActions
sx={{
@ -991,7 +1304,7 @@ export const TradeOffers: React.FC<any> = ({ foreignCoinBalance }: any) => {
<Button
variant="outlined"
onClick={() => {
setOpenShowOfferDetails(null)
setOpenShowOfferDetails(null);
}}
autoFocus
>
@ -1008,13 +1321,13 @@ const TradeRow = ({
value,
extra,
enableSlice,
enableCopy
enableCopy,
}: {
label: string;
value: string;
extra?: string;
enableSlice?: boolean
enableCopy?: boolean
enableSlice?: boolean;
enableCopy?: boolean;
}) => (
<Box
sx={{
@ -1036,7 +1349,9 @@ const TradeRow = ({
}}
>
<Typography variant="body2" sx={{ fontWeight: 500 }}>
{enableSlice && value?.length > 18 ? value?.slice(0, 6) + "..." + value.slice(-4) : value}
{enableSlice && value?.length > 18
? value?.slice(0, 6) + "..." + value.slice(-4)
: value}
</Typography>
{enableCopy && (
<Tooltip title="Copy">
@ -1054,34 +1369,30 @@ const TradeRow = ({
</Box>
);
const SelectWithInfoCell = ({ selectTradeForDetails }) => {
const handleInfoClick = (e: React.MouseEvent) => {
e.stopPropagation(); // Prevents row selection
selectTradeForDetails()
selectTradeForDetails();
// alert(`Info for ${data.qortalAtAddress}`); // Replace with your own UI
};
return (
<div className="ag-cell-ignore-selection" style={{ display: "flex", alignItems: "center", gap: 6 }}>
<div
className="ag-cell-ignore-selection"
style={{ display: "flex", alignItems: "center", gap: 6 }}
>
<IconButton
size="small"
onClick={handleInfoClick}
onClickCapture={(e) => {
e.stopPropagation();
handleInfoClick(e)
handleInfoClick(e);
}}
onMouseDown={(e) => e.stopPropagation()} // 👈 this is key
sx={{ minWidth: 0, padding: "0 4px" }}
>
<InfoOutlineIcon />
</IconButton>
</div>
);
};

View File

@ -214,10 +214,12 @@ export const CoinReceiveBtn = styled(Button)(({ theme }) => ({
export const CoinSelectRow = styled(Box)({
display: "flex",
flexDirection: "column",
flexDirection: "row",
gap: "5px",
alignSelf: "flex-start",
marginBottom: "5px"
justifyContent: "flex-start",
marginBottom: "5px",
width: '100%',
flexWrap: 'wrap'
});
export const CoinActionContainer = styled(Box)({

View File

@ -1,4 +1,11 @@
import { useState, useEffect, useRef, useContext, ChangeEvent, useMemo } from "react";
import {
useState,
useEffect,
useRef,
useContext,
ChangeEvent,
useMemo,
} from "react";
import {
BubbleCardColored1,
CoinActionContainer,
@ -26,15 +33,15 @@ import { cropAddress } from "../../utils/cropAddress";
import qtradeLogo from "../../components/common/icons/qtradeLogo.png";
import qortIcon from "../../assets/img/qort.png";
import ErrorIcon from "@mui/icons-material/Error";
import { CopyToClipboard } from "react-copy-to-clipboard";
import Copy from "../../assets/SVG/Copy.svg";
import {AddressQRCode} from './AddressQRCode'
import {FallingLines} from 'react-loader-spinner'
import { AddressQRCode } from "./AddressQRCode";
import { FallingLines } from "react-loader-spinner";
import {
Alert,
AppBar,
Avatar,
Box,
ButtonBase,
Card,
CardContent,
FormControl,
@ -58,6 +65,8 @@ import arrrIcon from "../../assets/img/arrr.png";
import { Spacer } from "../common/Spacer";
import { ReusableModal } from "../common/reusable-modal/ReusableModal";
import { NotificationContext } from "../../contexts/notificationContext";
import UnsignedFees from "../sell/UnsignedFees";
import { FeeManager } from "../sell/FeeManager";
const checkIfLocal = async () => {
try {
@ -140,7 +149,13 @@ const SelectRow = ({ coin }) => {
);
};
export const Header = ({ qortBalance, foreignCoinBalance }: any) => {
export const Header = ({
qortBalance,
foreignCoinBalance,
qortAddress,
fee,
setFee
}: any) => {
const [openDropdown, setOpenDropdown] = useState<boolean>(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLDivElement>(null);
@ -167,7 +182,6 @@ export const Header = ({ qortBalance, foreignCoinBalance }: any) => {
useContext(gameContext);
const { setNotification } = useContext(NotificationContext);
const LocalNodeSwitch = styled(Switch)(({ theme }) => ({
padding: 8,
"& .MuiSwitch-track": {
@ -241,29 +255,30 @@ export const Header = ({ qortBalance, foreignCoinBalance }: any) => {
const sendCoin = async () => {
try {
const coin = openCoinActionModal.coin === "QORT" ? 'QORT' : getCoinLabel()
if(!coin) return
const coin =
openCoinActionModal.coin === "QORT" ? "QORT" : getCoinLabel();
if (!coin) return;
setOpen(true);
setInfo({
type: "info",
message: "Sending Coin...",
autoHideDurationOff: true
autoHideDurationOff: true,
});
const response = await qortalRequest({
action: "SEND_COIN",
coin,
destinationAddress: senderAddress,
amount: +amount
amount: +amount,
});
if (response?.error) {
throw new Error(response?.error || "Failed to send coin.")
throw new Error(response?.error || "Failed to send coin.");
}
setOpen(true);
setInfo({
type: "success",
message: "Coin sent",
});
setAmount('')
setAmount("");
} catch (error) {
setOpen(true);
setInfo({
@ -271,7 +286,7 @@ export const Header = ({ qortBalance, foreignCoinBalance }: any) => {
message: error?.error || error?.message,
});
}
}
};
return (
<>
@ -428,12 +443,10 @@ export const Header = ({ qortBalance, foreignCoinBalance }: any) => {
}}
/>
{foreignCoinBalance === null ? (
<FallingLines
color="white"
width="30"
visible={true}
/>
) : foreignCoinBalance}{" "}
<FallingLines color="white" width="30" visible={true} />
) : (
foreignCoinBalance
)}{" "}
{getCoinLabel()}
</Box>
<CoinActionsRow>
@ -467,7 +480,10 @@ export const Header = ({ qortBalance, foreignCoinBalance }: any) => {
<Select
size="small"
value={selectedCoin}
onChange={(e) => setSelectedCoin(e.target.value)}
onChange={(e) => {
setFee(null)
setSelectedCoin(e.target.value)
}}
>
<MenuItem value={"LITECOIN"}>
<SelectRow coin="LTC" />
@ -488,6 +504,16 @@ export const Header = ({ qortBalance, foreignCoinBalance }: any) => {
<SelectRow coin="ARRR" />
</MenuItem>
</Select>
{!isUsingGateway && selectedCoin !== 'PIRATECHAIN' && (
<>
<FeeManager selectedCoin={selectedCoin} fee={fee}
setFee={setFee} />
<UnsignedFees
qortAddress={qortAddress}
selectedCoin={selectedCoin}
/>
</>
)}
</CoinSelectRow>
<Snackbar
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
@ -505,19 +531,19 @@ export const Header = ({ qortBalance, foreignCoinBalance }: any) => {
{info?.message}
</Alert>
)}
</Snackbar>
{openCoinActionModal && (
<ReusableModal
onClickClose={() => {
setOpenCoinActionModal(null);
setAmount('')
setSenderAddress('')
setAmount("");
setSenderAddress("");
}}
backdrop
>
<CoinActionContainer>
{openCoinActionModal.type === "send" ? <>
{openCoinActionModal.type === "send" ? (
<>
<CoinActionRow>
<HeaderRow>
{openCoinActionModal.type === "send" &&
@ -553,12 +579,20 @@ export const Header = ({ qortBalance, foreignCoinBalance }: any) => {
style={{ flexGrow: 1 }}
name={
openCoinActionModal.type === "send"
? `${openCoinActionModal.coin === "QORT" ? 'Recipient Address or Name' : 'Recipient Address'}`
? `${
openCoinActionModal.coin === "QORT"
? "Recipient Address or Name"
: "Recipient Address"
}`
: "Receive Address"
}
label={
openCoinActionModal.type === "send"
? `${openCoinActionModal.coin === "QORT" ? 'Recipient Address or Name' : 'Recipient Address'}`
? `${
openCoinActionModal.coin === "QORT"
? "Recipient Address or Name"
: "Recipient Address"
}`
: "Receive Address"
}
variant="filled"
@ -587,31 +621,40 @@ export const Header = ({ qortBalance, foreignCoinBalance }: any) => {
label="Amount"
variant="filled"
type="number"
value={
amount
}
value={amount}
required
onChange={(e) => {
setAmount(e.target.value)
setAmount(e.target.value);
}}
/>
</FormControl>
</CoinActionRow>
)}
</> : (
</>
) : (
<>
<ReceiveCoin setOpen={setOpen} setInfo={setInfo} coinAddresses={coinAddresses} setCoinAddresses={setCoinAddresses} selectedCoin={openCoinActionModal.coin === "QORT" ? 'QORT' :getCoinLabel()} />
<ReceiveCoin
setOpen={setOpen}
setInfo={setInfo}
coinAddresses={coinAddresses}
setCoinAddresses={setCoinAddresses}
selectedCoin={
openCoinActionModal.coin === "QORT"
? "QORT"
: getCoinLabel()
}
/>
</>
)}
{openCoinActionModal.type === 'send' && (
{openCoinActionModal.type === "send" && (
<CoinActionRow style={{ gap: "10px" }}>
{/* <CoinCancelBtn onClick={() => setOpenCoinActionModal(null)}>
Cancel
</CoinCancelBtn> */}
<CoinConfirmSendBtn
onClick={() => {
if(openCoinActionModal.type === 'send'){
sendCoin()
if (openCoinActionModal.type === "send") {
sendCoin();
}
setNotification({
alertType: "alertInfo",
@ -623,14 +666,10 @@ export const Header = ({ qortBalance, foreignCoinBalance }: any) => {
</CoinConfirmSendBtn>
</CoinActionRow>
)}
</CoinActionContainer>
</ReusableModal>
)}
</HeaderNav>
</>
);
};
@ -660,15 +699,19 @@ transition: all 0.2s;
fill: white; // Fill color changes to white on hover
}
}
`;
`
const ReceiveCoin = ({coinAddresses, setCoinAddresses, selectedCoin, setOpen, setInfo})=> {
const [errorMsg, setErrorMsg] = useState('')
const ReceiveCoin = ({
coinAddresses,
setCoinAddresses,
selectedCoin,
setOpen,
setInfo,
}) => {
const [errorMsg, setErrorMsg] = useState("");
const foreignAddress = useMemo(() => {
return coinAddresses[selectedCoin] || null
}, [coinAddresses, selectedCoin])
return coinAddresses[selectedCoin] || null;
}, [coinAddresses, selectedCoin]);
const getForeignAddress = async (coin) => {
try {
@ -679,56 +722,69 @@ const ReceiveCoin = ({coinAddresses, setCoinAddresses, selectedCoin, setOpen, se
});
const response = await qortalRequest({
action: "GET_USER_WALLET",
coin
coin,
});
if (response?.address) {
setCoinAddresses((prev) => {
return {
...prev,
[coin]: response.address
}
})
[coin]: response.address,
};
});
}
if (response?.error) {
throw new Error(response?.error || "Failed to send coin.")
throw new Error(response?.error || "Failed to send coin.");
}
} catch (error) {
setErrorMsg(error?.message)
setErrorMsg(error?.message);
} finally {
setOpen(false);
setInfo(null);
}
}
};
useEffect(() => {
if(!selectedCoin) return
if (!selectedCoin) return;
if (!coinAddresses[selectedCoin]) {
getForeignAddress(selectedCoin)
getForeignAddress(selectedCoin);
}
}, [selectedCoin, coinAddresses])
}, [selectedCoin, coinAddresses]);
return (
<Box sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}>
<Typography sx={{
color: 'white'
}}>{`Send ${selectedCoin} to your address below`}</Typography>
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Typography
sx={{
color: "white",
}}
>{`Send ${selectedCoin} to your address below`}</Typography>
<Spacer height="20px" />
{foreignAddress && (
<CopyToClipboard text={foreignAddress} onCopy={()=> {
<ButtonBase
onClick={() => {
navigator.clipboard
.writeText(foreignAddress)
.then(() => {
setOpen(true);
setInfo({
type: "info",
message: "Address copied!",
});
}}>
})
.catch((err) => {
console.error("Failed to copy LTC address:", err);
});
}}
>
<AddressBox>
{foreignAddress} <img src={Copy} />
</AddressBox>
</CopyToClipboard>
</ButtonBase>
)}
{foreignAddress && (
<>
@ -738,11 +794,15 @@ const ReceiveCoin = ({coinAddresses, setCoinAddresses, selectedCoin, setOpen, se
{errorMsg && (
<>
<Spacer height="20px" />
<Typography sx={{
color: 'white'
}}>{errorMsg}</Typography>
<Typography
sx={{
color: "white",
}}
>
{errorMsg}
</Typography>
</>
)}
</Box>
)
}
);
};

View File

@ -23,8 +23,8 @@ import TradeBotList from "./TradeBotList";
export const CustomLabel = styled(InputLabel)`
font-weight: 400;
font-family: Inter;
font-size: 10px;
line-height: 12px;
font-size: 14px;
line-height: 1.2;
color: rgba(255, 255, 255, 0.5);
`;
@ -61,12 +61,12 @@ export const CustomInput = styled(TextField)({
// backgroundColor: "rgba(30, 30, 32, 1)",
outline: "none",
input: {
fontSize: 10,
fontSize: '14px',
fontFamily: "Inter",
fontWeight: 400,
color: "white",
"&::placeholder": {
fontSize: 16,
fontSize: '14px',
color: "rgba(255, 255, 255, 0.2)",
},
outline: "none",
@ -263,7 +263,7 @@ export const CreateSell = ({ qortAddress, show }) => {
onChange={(e) => setQortAmount(+e.target.value)}
autoComplete="off"
/>
<Spacer height="6px" />
<Spacer height="15px" />
<CustomLabel htmlFor="standard-adornment-amount">
{`Price of Each QORT (in ${getCoinLabel()})`}
</CustomLabel>
@ -275,7 +275,7 @@ export const CreateSell = ({ qortAddress, show }) => {
onChange={(e) => setForeignAmount(+e.target.value)}
autoComplete="off"
/>
<Spacer height="6px" />
<Spacer height="15px" />
<Typography>
{`${qortAmount * foreignAmount} ${getCoinLabel()}`} for{" "}
{qortAmount} QORT

View File

@ -0,0 +1,374 @@
import React, {
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import gameContext from "../../contexts/gameContext";
import {
Alert,
Box,
ButtonBase,
Snackbar,
SnackbarCloseReason,
ToggleButton,
ToggleButtonGroup,
Typography,
} from "@mui/material";
import ChangeCircleIcon from "@mui/icons-material/ChangeCircle";
import { ReusableModal } from "../common/reusable-modal/ReusableModal";
import QuestionMarkIcon from "@mui/icons-material/QuestionMark";
import {
CoinActionContainer,
CoinActionRow,
HeaderRow,
} from "../header/Header-styles";
import { CustomInput, CustomLabel } from "./CreateSell";
import { Spacer } from "../common/Spacer";
import { usePublish, Service, QortalGetMetadata } from "qapp-core";
import { SetLeftFeature } from "ag-grid-community";
function calculateFeeFromRate(feePerKb, sizeInBytes) {
const fee = (feePerKb / 1000) * sizeInBytes;
return fee?.toFixed(0);
}
function calculateRateFromFee(totalFee, sizeInBytes) {
return (totalFee / sizeInBytes) * 1000;
}
export const FeeManager = ({ selectedCoin, setFee, fee }) => {
const [feeLocation, setFeeLocation] = useState<QortalGetMetadata>({
name: "",
identifier: "",
service: "JSON",
});
const { resource } = usePublish(3, "JSON", feeLocation);
const [editFee, setEditFee] = useState("");
const [openModal, setOpenModal] = useState(false);
const [recommendedFee, setRecommendedFee] = useState("m");
const [openAlert, setOpenAlert] = useState(false);
const [info, setInfo] = useState<any>(null);
const { getCoinLabel } = useContext(gameContext);
console.log("editFee", editFee);
const handleCloseAlert = (
event?: React.SyntheticEvent | Event,
reason?: SnackbarCloseReason
) => {
if (reason === "clickaway") {
return;
}
setOpenAlert(false);
setInfo(null);
};
const coin = useMemo(() => {
return getCoinLabel(selectedCoin)?.toLowerCase();
}, [selectedCoin, getCoinLabel]);
const establishUpdateFeeForm = useCallback(async (coin) => {
setFee("");
// if the coin or type is not set, then abort
if (!coin) {
return;
}
console.log("selectedCoin", coin);
// const coinRequest = coin.current.toLowerCase();
const typeRequest = "feerequired";
try {
const response = await qortalRequestWithTimeout(
{
action: "GET_FOREIGN_FEE",
coin: coin,
type: typeRequest,
},
1800000
);
if (response && !isNaN(+response)) {
setFee(response);
}
console.log("response", response);
} catch (error) {
setFee("");
console.error(error);
}
}, []);
useEffect(() => {
establishUpdateFeeForm(coin);
console.log("editFee or fetch changed");
}, [coin, establishUpdateFeeForm]);
const recommendedFeeData = useMemo(() => {
if (!resource?.data) return null;
if (
!resource?.data?.["BTC"] ||
!resource?.data?.["LTC"] ||
!resource?.data?.["DOGE"]
)
return null;
return resource.data;
}, [resource?.data]);
const recommendedFeeDisplay = useMemo(() => {
if (!selectedCoin || !recommendedFeeData) return;
const coin = getCoinLabel(selectedCoin)?.toUpperCase();
return recommendedFeeData[coin][recommendedFee];
}, [recommendedFeeData, recommendedFee, selectedCoin]);
const updateFee = async () => {
const typeRequest = "feerequired";
try {
let feeToSave = editFee
if(recommendedFee !== 'custom'){
feeToSave = calculateFeeFromRate(recommendedFeeDisplay, 300)
}
console.log('feeToSave', feeToSave)
const response = await qortalRequestWithTimeout(
{
action: "UPDATE_FOREIGN_FEE",
coin: coin,
type: typeRequest,
value: feeToSave,
},
1800000
);
if (response && !isNaN(+response)) {
setFee(response);
setOpenAlert(true);
setInfo({
type: "success",
message: "Fee updated!",
});
setOpenModal(false);
} else throw new Error("Unable to update fee");
} catch (error) {
setOpenAlert(true);
setInfo({
type: "error",
message: error?.message || "Unable to update fee",
});
}
};
const handleChange = (
event: React.MouseEvent<HTMLElement>,
newAlignment: string
) => {
if(newAlignment){
setRecommendedFee(newAlignment);
}
};
const getLatestFees = useCallback(async () => {
try {
const res = await fetch(
`http://devnet-nodes.qortal.link:11112/arbitrary/resources/searchsimple?service=JSON&identifier=foreign-fee&name=Foreign-Fee-Publisher&prefix=true&limit=1&reverse=true`
);
const data = await res.json();
if (data && data?.length > 0) {
setFeeLocation(data[0]);
}
} catch (error) {}
}, []);
useEffect(() => {
getLatestFees();
}, [getLatestFees]);
if (!fee) return;
return (
<>
<ButtonBase
onClick={() => {
setOpenModal(true);
setEditFee(fee);
}}
sx={{
minHeight: "42px",
border: "1px solid gray",
color: "white",
display: "flex",
alignItems: "center",
padding: "5px 5px",
gap: "10px",
borderRadius: "5px",
"&:hover": {
border: "1px solid white", // Border color on hover
},
}}
>
<Typography>Fee: {fee}</Typography>
<ChangeCircleIcon
sx={{
color: "white",
}}
/>
</ButtonBase>
{openModal && (
<ReusableModal
onClickClose={() => {
setOpenModal(false);
setEditFee(fee);
}}
backdrop
>
<CoinActionContainer sx={{
width: '450px',
maxWidth: '95vw'
}}>
<CoinActionRow>
<HeaderRow>
<Typography
variant="h4"
sx={{
color: "white",
}}
>
Update unlocking fee for {selectedCoin}
</Typography>
</HeaderRow>
</CoinActionRow>
<CoinActionRow>
<HeaderRow>
<Box sx={{
width: '100%'
}}>
<Box sx={{
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}>
<CustomLabel htmlFor="standard-adornment-name">
Recommended fee selection
</CustomLabel>
<Spacer height="10px" />
<ToggleButtonGroup
color="primary"
value={recommendedFee}
exclusive
onChange={handleChange}
aria-label="Platform"
>
<ToggleButton value="l">Low</ToggleButton>
<ToggleButton value="m">Medium</ToggleButton>
<ToggleButton value="h">High</ToggleButton>
<ToggleButton value="custom">Custom</ToggleButton>
</ToggleButtonGroup>
</Box>
{recommendedFeeDisplay && (
<>
<Spacer height="15px" />
<Typography
sx={{
color: "white",
fontSize: "18px",
}}
>
New fee:{" "}
{calculateFeeFromRate(recommendedFeeDisplay, 300)}
</Typography>
<Spacer height="10px" />
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "20px",
}}
>
<QuestionMarkIcon
sx={{
color: "white",
}}
/>
<Typography
sx={{
color: "white",
}}
>
This recommended fee is derived from{" "}
{recommendedFeeDisplay} per kb, for a transaction that
is approximately 300 kB in size.
</Typography>
</Box>
</>
)}
</Box>
</HeaderRow>
</CoinActionRow>
{recommendedFee === "custom" && (
<CoinActionRow>
<HeaderRow>
<Box>
<CustomLabel htmlFor="standard-adornment-name">
Custom fee
</CustomLabel>
<Spacer height="5px" />
<CustomInput
id="standard-adornment-name"
type="number"
value={editFee}
onChange={(e) => setEditFee(e.target.value)}
autoComplete="off"
/>
</Box>
</HeaderRow>
</CoinActionRow>
)}
<ButtonBase
onClick={updateFee}
disabled={(recommendedFee === 'custom' && !editFee)}
sx={{
minHeight: "42px",
border: "1px solid gray",
color: "white",
display: "flex",
alignItems: "center",
padding: "5px 20px",
gap: "10px",
borderRadius: "5px",
"&:hover": {
border: "1px solid white", // Border color on hover
},
}}
>
<ChangeCircleIcon
sx={{
color: "white",
}}
/>
<Typography>Update fee</Typography>
</ButtonBase>
</CoinActionContainer>
</ReusableModal>
)}
<Snackbar
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
open={openAlert}
onClose={handleCloseAlert}
autoHideDuration={6000}
>
<Alert
onClose={handleCloseAlert}
severity={info?.type}
variant="filled"
sx={{ width: "100%" }}
>
{info?.message}
</Alert>
</Snackbar>
</>
);
};

View File

@ -0,0 +1,245 @@
import React, {
useCallback,
useContext,
useEffect,
useRef,
useState,
} from "react";
import { baseLocalHost } from "../Grids/TradeOffers";
import {
Alert,
Box,
ButtonBase,
Snackbar,
SnackbarCloseReason,
Typography,
} from "@mui/material";
import TouchAppIcon from "@mui/icons-material/TouchApp";
import { ReusableModal } from "../common/reusable-modal/ReusableModal";
import {
CoinActionContainer,
CoinActionRow,
CoinConfirmSendBtn,
HeaderRow,
} from "../header/Header-styles";
export default function UnsignedFees({ qortAddress }) {
const [isPositive, setIsPositive] = useState(null);
const [openModal, setOpenModal] = useState(false);
const [openAlert, setOpenAlert] = useState(false);
const [info, setInfo] = useState<any>(null);
console.log("isPositive", isPositive);
const qortAddressRef = useRef(null);
const handleCloseAlert = (
event?: React.SyntheticEvent | Event,
reason?: SnackbarCloseReason
) => {
if (reason === "clickaway") {
return;
}
setOpenAlert(false);
setInfo(null);
};
useEffect(() => {
if (qortAddress) {
qortAddressRef.current = qortAddress;
}
}, [qortAddress]);
const restartUnsignedFeeSocket = () => {
setTimeout(() => initUnsignedFeeSocket(true), 50);
};
const restartFeeData = () => {
if (socketRef.current) {
socketRef.current.close(1000, "forced"); // Close with a custom reason
socketRef.current = null;
}
setIsPositive(null);
};
const socketRef = useRef(null);
const initUnsignedFeeSocket = (restarted = false) => {
let socketTimeout: any;
const socketLink = `${
window.location.protocol === "https:" ? "wss:" : "ws:"
}//${baseLocalHost}/websockets/crosschain/unsignedfees`;
socketRef.current = new WebSocket(socketLink);
socketRef.current.onopen = () => {
setTimeout(pingSocket, 50);
};
socketRef.current.onmessage = (e) => {
restarted = false;
const data = JSON.parse(e.data);
if (qortAddressRef.current === data.address) {
if (data.positive) {
setIsPositive(true);
setOpenModal(true);
} else {
setIsPositive(false);
}
}
};
socketRef.current.onclose = (event) => {
clearTimeout(socketTimeout);
if (event.reason === "forced") {
return;
}
restartUnsignedFeeSocket();
};
socketRef.current.onerror = (e) => {
clearTimeout(socketTimeout);
};
const pingSocket = () => {
socketRef.current.send("ping");
socketTimeout = setTimeout(pingSocket, 295000);
};
};
const getUnsignedFees = useCallback(async (address)=> {
try {
const url = `http://devnet-nodes.qortal.link:11112/crosschain/unsignedfees/${address}`
const res = await fetch(url)
const data = await res.json()
if(data && data.length > 0){
setIsPositive(true)
} else {
setIsPositive(false)
}
} catch (error) {
console.error(error)
}
}, [])
useEffect(() => {
if (!qortAddress) return;
restartFeeData();
getUnsignedFees(qortAddress)
setTimeout(() => {
initUnsignedFeeSocket();
}, 500);
return () => {
if (socketRef.current) {
socketRef.current.close(1000, "forced");
}
};
}, [qortAddress, getUnsignedFees]);
const signForeignFees = async () => {
try {
await qortalRequest({
action: "SIGN_FOREIGN_FEES",
});
} catch (error) {
console.error(error);
}
};
if (!isPositive) return null;
return (
<>
<ButtonBase
onClick={signForeignFees}
sx={{
minHeight: "42px",
background: "red",
color: "white",
display: "flex",
alignItems: "center",
padding: "5px 20px",
gap: "20px",
borderRadius: "5px",
"&:hover": {
background: "darkred", // Border color on hover
},
}}
>
<TouchAppIcon
sx={{
color: "white",
}}
/>
<Typography>Action required! You have unsigned fees</Typography>
</ButtonBase>
{openModal && (
<ReusableModal
onClickClose={() => {
setOpenModal(false);
}}
backdrop
>
<CoinActionContainer>
<CoinActionRow>
<HeaderRow>
<Typography
variant="h2"
sx={{
color: "white",
}}
>
Action required!
</Typography>
</HeaderRow>
</CoinActionRow>
<CoinActionRow>
<HeaderRow>
<Typography
variant="h4"
sx={{
color: "white",
}}
>
In order for buyers to use the correct unlocking fee, please
sign your fees.
</Typography>
</HeaderRow>
</CoinActionRow>
<ButtonBase
onClick={signForeignFees}
sx={{
minHeight: "42px",
background: "red",
color: "white",
display: "flex",
alignItems: "center",
padding: "5px 20px",
gap: "20px",
borderRadius: "5px",
"&:hover": {
background: "darkred", // Border color on hover
},
}}
>
<TouchAppIcon
sx={{
color: "white",
}}
/>
<Typography>Sign Fees</Typography>
</ButtonBase>
</CoinActionContainer>
</ReusableModal>
)}
<Snackbar
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
open={openAlert}
onClose={handleCloseAlert}
autoHideDuration={6000}
>
<Alert
onClose={handleCloseAlert}
severity={info?.type}
variant="filled"
sx={{ width: "100%" }}
>
{info?.message}
</Alert>
</Snackbar>
</>
);
}

View File

@ -1,5 +1,5 @@
import { BrowserRouter } from 'react-router-dom'
import ReactDOM from 'react-dom/client'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import "./index.scss";
import { IndexedDBProvider } from './contexts/indexedDBContext.tsx';
@ -11,7 +11,7 @@ interface CustomWindow extends Window {
const customWindow = window as unknown as CustomWindow;
const baseUrl = customWindow?._qdnBase || "";
ReactDOM.createRoot(document.getElementById('root')!).render(
createRoot(document.getElementById('root')!).render(
<BrowserRouter basename={baseUrl}>
<IndexedDBProvider>
<App />

View File

@ -24,6 +24,8 @@ export const HomePage = () => {
} = useContext(gameContext);
const { setNotification } = useContext(NotificationContext);
const [mode, setMode] = useState("buy");
const [fee, setFee] = useState("");
const filteredOngoingTrades = useMemo(() => {
return onGoingTrades?.filter(
(item) => item?.tradeInfo?.foreignBlockchain === selectedCoin
@ -50,6 +52,9 @@ export const HomePage = () => {
<Header
qortBalance={qortBalance}
foreignCoinBalance={foreignCoinBalance}
qortAddress={userInfo?.address}
fee={fee}
setFee={setFee}
/>
<AppContainer>
@ -108,7 +113,8 @@ export const HomePage = () => {
</TextTableTitle>
</Box>
<Spacer height="10px" />
<TradeOffers foreignCoinBalance={foreignCoinBalance} />
<TradeOffers fee={fee}
setFee={setFee} foreignCoinBalance={foreignCoinBalance} />
</div>
<CreateSell show={mode === "sell"} qortAddress={userInfo?.address} />

View File

@ -1,42 +0,0 @@
import { io, Socket } from "socket.io-client";
import { DefaultEventsMap } from '@socket.io/component-emitter';
class SocketService {
public socket: Socket | null = null;
public connect(
url: string,
token: string
): Promise<Socket<DefaultEventsMap, DefaultEventsMap>> {
return new Promise((resolve, reject) => {
this.socket = io(url, {
auth: {
token: token
}
});
if (!this.socket) return reject("Socket initialization failed");
this.socket.on("connect", () => {
console.log('connected');
resolve(this.socket as Socket);
});
this.socket.on("connect_error", (err) => {
console.log("Connection error: ", err);
reject(err);
});
});
}
public disconnect() {
if (this.socket) {
this.socket.disconnect();
this.socket = null; // Optionally reset the socket to null after disconnecting
console.log('Disconnected');
}
}
}
export default new SocketService();