From 0a3786b85eeb4360a2c6cd337acda7ab37a0042d Mon Sep 17 00:00:00 2001 From: Uncle Fatso Date: Fri, 22 Aug 2025 18:00:48 +0300 Subject: [PATCH] implemented ability to watch cross-chain transaction status Signed-off-by: Uncle Fatso --- package.json | 11 +- pnpm-lock.yaml | 336 +++++++++++- src/App.jsx | 19 + src/containers/Bridge/Bridge.jsx | 478 ++++++++++-------- src/hooks/ghost/MetadataProvider.jsx | 44 ++ src/hooks/ghost/UnstableProvider.jsx | 55 ++ src/hooks/ghost/index.js | 9 + src/hooks/ghost/useApplauseThreshold.js | 23 + src/hooks/ghost/useApplausesForTransaction.js | 47 ++ src/hooks/ghost/useAuthorities.js | 41 ++ src/hooks/ghost/useClapsInSession.js | 41 ++ src/hooks/ghost/useCurrentIndex.js | 41 ++ src/hooks/ghost/useEvmNetwork.js | 41 ++ src/hooks/ghost/useReceivedClaps.js | 47 ++ src/main.jsx | 7 +- 15 files changed, 1016 insertions(+), 224 deletions(-) create mode 100644 src/hooks/ghost/MetadataProvider.jsx create mode 100644 src/hooks/ghost/UnstableProvider.jsx create mode 100644 src/hooks/ghost/index.js create mode 100644 src/hooks/ghost/useApplauseThreshold.js create mode 100644 src/hooks/ghost/useApplausesForTransaction.js create mode 100644 src/hooks/ghost/useAuthorities.js create mode 100644 src/hooks/ghost/useClapsInSession.js create mode 100644 src/hooks/ghost/useCurrentIndex.js create mode 100644 src/hooks/ghost/useEvmNetwork.js create mode 100644 src/hooks/ghost/useReceivedClaps.js diff --git a/package.json b/package.json index bd7fb17..91ccdd3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ghost-dao-interface", "private": true, - "version": "0.2.13", + "version": "0.2.14", "type": "module", "scripts": { "dev": "vite", @@ -18,8 +18,14 @@ "@mui/icons-material": "^6.4.7", "@mui/material": "^6.4.7", "@mui/utils": "^6.4.6", + "@polkadot-api/metadata-builders": "0.13.0", + "@polkadot-api/observable-client": "~0.8.6", + "@polkadot-api/substrate-bindings": "0.15.0", + "@polkadot-api/substrate-client": "~0.3.0", "@polkadot-api/utils": "~0.1.2", "@polkadot-labs/hdkd-helpers": "^0.0.20", + "@polkadot/util-crypto": "^13.5.5", + "@substrate/connect-discovery": "^0.2.2", "@tanstack/react-query": "^5.67.2", "@tanstack/react-query-devtools": "^5.67.2", "@wagmi/core": "^2.17.3", @@ -29,7 +35,10 @@ "react-helmet": "^6.1.0", "react-hot-toast": "^2.5.2", "react-router-dom": "^7.3.0", + "rxjs": "7.8.1", + "scale-ts": "^1.6.1", "scss": "^0.2.4", + "swr": "2.2.5", "viem": "^2.31.0", "wagmi": "^2.15.6" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d27005f..183862c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,12 +32,30 @@ importers: '@mui/utils': specifier: ^6.4.6 version: 6.4.6(@types/react@19.0.10)(react@19.0.0) + '@polkadot-api/metadata-builders': + specifier: 0.13.0 + version: 0.13.0 + '@polkadot-api/observable-client': + specifier: ~0.8.6 + version: 0.8.6(@polkadot-api/substrate-client@0.3.0)(rxjs@7.8.1) + '@polkadot-api/substrate-bindings': + specifier: 0.15.0 + version: 0.15.0 + '@polkadot-api/substrate-client': + specifier: ~0.3.0 + version: 0.3.0 '@polkadot-api/utils': - specifier: 0.1.2 + specifier: ~0.1.2 version: 0.1.2 '@polkadot-labs/hdkd-helpers': specifier: ^0.0.20 version: 0.0.20 + '@polkadot/util-crypto': + specifier: ^13.5.5 + version: 13.5.5(@polkadot/util@13.5.5) + '@substrate/connect-discovery': + specifier: ^0.2.2 + version: 0.2.2 '@tanstack/react-query': specifier: ^5.67.2 version: 5.67.2(react@19.0.0) @@ -65,9 +83,18 @@ importers: react-router-dom: specifier: ^7.3.0 version: 7.3.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + rxjs: + specifier: 7.8.1 + version: 7.8.1 + scale-ts: + specifier: ^1.6.1 + version: 1.6.1 scss: specifier: ^0.2.4 version: 0.2.4 + swr: + specifier: 2.2.5 + version: 2.2.5(react@19.0.0) viem: specifier: ^2.31.0 version: 2.31.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)(zod@3.22.4) @@ -725,10 +752,6 @@ packages: resolution: {integrity: sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==} engines: {node: ^14.21.3 || >=16} - '@noble/curves@1.9.2': - resolution: {integrity: sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==} - engines: {node: ^14.21.3 || >=16} - '@noble/curves@1.9.6': resolution: {integrity: sha512-GIKz/j99FRthB8icyJQA51E8Uk5hXmdyThjgQXRKiv9h0zeRlzSCLIzFw6K1LotZ3XuB7yzlf76qk7uBmTdFqA==} engines: {node: ^14.21.3 || >=16} @@ -753,12 +776,115 @@ packages: resolution: {integrity: sha512-IHnV6A+zxU7XwmKFinmYjUcwlyK9+xkG3/s9KcQhI9BjQKycrJ1JRO+FbNYPwZiPKW3je/DR0k7w8/gLa5eaxQ==} deprecated: 'The package is now available as "qr": npm install qr' + '@polkadot-api/json-rpc-provider@0.0.4': + resolution: {integrity: sha512-9cDijLIxzHOBuq6yHqpqjJ9jBmXrctjc1OFqU+tQrS96adQze3mTIH6DTgfb/0LMrqxzxffz1HQGrIlEH00WrA==} + + '@polkadot-api/metadata-builders@0.10.2': + resolution: {integrity: sha512-rtdihBFd25oT9/71Q+EOR9q6E6mCl1pPe/2He/LtlY0TyHiYqO2KpMZNXkoGcw1RHvrV+CAtDFMvK1j3n8aW8w==} + + '@polkadot-api/metadata-builders@0.13.0': + resolution: {integrity: sha512-z8AT0mbOniHmiI0sNjQSUtOXGLHTqFNV8bqdyes1WAMbpBEIcnTlQxhIrJkpPEQCh7kLNTHqa3OQjSIrOH7X1Q==} + + '@polkadot-api/observable-client@0.8.6': + resolution: {integrity: sha512-ci5HC8TYjGxoTG/QM+LLuGrfIsn+dtR7BBQz483c/ML8K/Hxl9v+evgZzPi9xNMwZ25mytn9lhA5dovYSEauSA==} + peerDependencies: + '@polkadot-api/substrate-client': 0.3.0 + rxjs: '>=7.8.0' + + '@polkadot-api/substrate-bindings@0.11.1': + resolution: {integrity: sha512-+oqAZB7y18KrP/DqKmU2P3nNmRzjCY7edtW7tyA1g1jPouF7HhRr/Q13lJseDX9sdE2FZGrKZtivzsw8XeXBng==} + + '@polkadot-api/substrate-bindings@0.15.0': + resolution: {integrity: sha512-w/GSQeEYfFqyM50s2c3KDfl/qumtLnk7EUALXER/eFWSawXYVeL3PCg95GZfDFrc7MODVmq+XeXUoZBFGHvodA==} + + '@polkadot-api/substrate-client@0.3.0': + resolution: {integrity: sha512-0hEvQLKH2zhaFzE8DPkWehvJilec8u2O2wbIEUStm0OJ8jIFtJ40MFjXQfB01dXBWUz1KaVBqS6xd3sZA90Dpw==} + '@polkadot-api/utils@0.1.2': resolution: {integrity: sha512-yhs5k2a8N1SBJcz7EthZoazzLQUkZxbf+0271Xzu42C5AEM9K9uFLbsB+ojzHEM72O5X8lPtSwGKNmS7WQyDyg==} + '@polkadot-api/utils@0.2.0': + resolution: {integrity: sha512-nY3i5fQJoAxU4n3bD7Fs208/KR2J95SGfVc58kDjbRYN5a84kWaGEqzjBNtP9oqht49POM8Bm9mbIrkvC1Bzuw==} + '@polkadot-labs/hdkd-helpers@0.0.20': resolution: {integrity: sha512-P3o1FpPqLACaHhDT/J6O3xYQIBdOs0FDJtZQI8/LGotgIGp85mKDnH/cSSK3QC2i67ZY/d/POs8K0jEspLMiGg==} + '@polkadot/networks@13.5.5': + resolution: {integrity: sha512-gTaKVSDRxjNAQ/oFMA83DXOo8A+/LP4XePbEHxNCku/Ox5R3IYGKTeZhlHgYtUZvdZgK+miyroEyz1Eq6Z9p+Q==} + engines: {node: '>=18'} + + '@polkadot/util-crypto@13.5.5': + resolution: {integrity: sha512-LAHarViiPwjrXl05fXOV5pW6jvK8A0Y6uIJnttSSERjTKqG5O4VtgRAcqLXShTp1rEVE5T4DaIX5xZd7azBHyg==} + engines: {node: '>=18'} + peerDependencies: + '@polkadot/util': 13.5.5 + + '@polkadot/util@13.5.5': + resolution: {integrity: sha512-O3sGI8vWmv5o1cd8fDkc+cZGpUsG+ZUFAOitgv6bRt5llaBqS5VpTrUANEjfgUMgUuTn7Y2cPKGDLItYr5WnUg==} + engines: {node: '>=18'} + + '@polkadot/wasm-bridge@7.4.1': + resolution: {integrity: sha512-tdkJaV453tezBxhF39r4oeG0A39sPKGDJmN81LYLf+Fihb7astzwju+u75BRmDrHZjZIv00un3razJEWCxze6g==} + engines: {node: '>=18'} + peerDependencies: + '@polkadot/util': '*' + '@polkadot/x-randomvalues': '*' + + '@polkadot/wasm-crypto-asmjs@7.4.1': + resolution: {integrity: sha512-pwU8QXhUW7IberyHJIQr37IhbB6DPkCG5FhozCiNTq4vFBsFPjm9q8aZh7oX1QHQaiAZa2m2/VjIVE+FHGbvHQ==} + engines: {node: '>=18'} + peerDependencies: + '@polkadot/util': '*' + + '@polkadot/wasm-crypto-init@7.4.1': + resolution: {integrity: sha512-AVka33+f7MvXEEIGq5U0dhaA2SaXMXnxVCQyhJTaCnJ5bRDj0Xlm3ijwDEQUiaDql7EikbkkRtmlvs95eSUWYQ==} + engines: {node: '>=18'} + peerDependencies: + '@polkadot/util': '*' + '@polkadot/x-randomvalues': '*' + + '@polkadot/wasm-crypto-wasm@7.4.1': + resolution: {integrity: sha512-PE1OAoupFR0ZOV2O8tr7D1FEUAwaggzxtfs3Aa5gr+yxlSOaWUKeqsOYe1KdrcjmZVV3iINEAXxgrbzCmiuONg==} + engines: {node: '>=18'} + peerDependencies: + '@polkadot/util': '*' + + '@polkadot/wasm-crypto@7.4.1': + resolution: {integrity: sha512-kHN/kF7hYxm1y0WeFLWeWir6oTzvcFmR4N8fJJokR+ajYbdmrafPN+6iLgQVbhZnDdxyv9jWDuRRsDnBx8tPMQ==} + engines: {node: '>=18'} + peerDependencies: + '@polkadot/util': '*' + '@polkadot/x-randomvalues': '*' + + '@polkadot/wasm-util@7.4.1': + resolution: {integrity: sha512-RAcxNFf3zzpkr+LX/ItAsvj+QyM56TomJ0xjUMo4wKkHjwsxkz4dWJtx5knIgQz/OthqSDMR59VNEycQeNuXzA==} + engines: {node: '>=18'} + peerDependencies: + '@polkadot/util': '*' + + '@polkadot/x-bigint@13.5.5': + resolution: {integrity: sha512-SAd7Lfdgp6mz+utkoML8MN9FqTMCuPfk7v5rLJnm9vHgXw5uYnycbjH5Uc7ZgQIQWtMXJV3thrlltMan5DUXtA==} + engines: {node: '>=18'} + + '@polkadot/x-global@13.5.5': + resolution: {integrity: sha512-fw+VM191bodacSeieMm8Vmrym4jjevX08IINDcQTd1gIOjtE5CriJhwfBbAF4WnlTp/11jhhbX4/SvWMubXAzQ==} + engines: {node: '>=18'} + + '@polkadot/x-randomvalues@13.5.5': + resolution: {integrity: sha512-W0AoNgr/NEVsHWegJUjUyI9Q1IoTHILIb/bkjyTcXTU3+2YFxP12ophhsI1dMaNbXqFotNyts7mNOsTVDnQNXA==} + engines: {node: '>=18'} + peerDependencies: + '@polkadot/util': 13.5.5 + '@polkadot/wasm-util': '*' + + '@polkadot/x-textdecoder@13.5.5': + resolution: {integrity: sha512-KkZ1rqdJZ8tsRY0D5pLqfU8B/BrSQVEPMKHj4s/oc8dTrikfEUC+ELaH2jdrUqsZX6K/OTHjaF0J31YZcr7rCg==} + engines: {node: '>=18'} + + '@polkadot/x-textencoder@13.5.5': + resolution: {integrity: sha512-yEgUUojBb4goYf4V5I7urdJ+W+1aI13U1kZmUwMc+/G2YQz8pX3s/Tyb/iuxU5MlFh0AZZXP5NqUnFol+vwNEg==} + engines: {node: '>=18'} + '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} @@ -935,6 +1061,15 @@ packages: '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@substrate/connect-discovery@0.2.2': + resolution: {integrity: sha512-IaW1VnSiNKE6ITzMaWu6kph52jIpSswegV4S6id/XnVpC5gqp/iBidmMy88REecatyktPZKBCwfyFxRDHeiijQ==} + + '@substrate/discovery@0.2.2': + resolution: {integrity: sha512-vD5B1LqIaiDBWpJ7h1b89jzhU0AKjOySFAt2zbTDz+gpRdcOn0I8PoxhOrent4LXFX9O/ch8bGG/JYDtr2B2QA==} + + '@substrate/ss58-registry@1.51.0': + resolution: {integrity: sha512-TWDurLiPxndFgKjVavCniytBIw+t4ViOi7TYp9h/D0NMmkEc9klFTo+827eyEJ0lELpqO207Ey7uGxUa+BS1jQ==} + '@svgr/babel-plugin-add-jsx-attribute@8.0.0': resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==} engines: {node: '>=14'} @@ -1032,6 +1167,9 @@ packages: '@types/babel__traverse@7.20.6': resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + '@types/bn.js@5.2.0': + resolution: {integrity: sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==} + '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} @@ -1319,6 +1457,9 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + cliui@6.0.0: resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} @@ -2252,8 +2393,8 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - rxjs@7.8.2: - resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} @@ -2512,6 +2653,11 @@ packages: svg-parser@2.0.4: resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} + swr@2.2.5: + resolution: {integrity: sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 + sync-child-process@1.0.2: resolution: {integrity: sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==} engines: {node: '>=16.0.0'} @@ -3574,10 +3720,6 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 - '@noble/curves@1.9.2': - dependencies: - '@noble/hashes': 1.8.0 - '@noble/curves@1.9.6': dependencies: '@noble/hashes': 1.8.0 @@ -3592,8 +3734,49 @@ snapshots: '@paulmillr/qr@0.2.1': {} + '@polkadot-api/json-rpc-provider@0.0.4': {} + + '@polkadot-api/metadata-builders@0.10.2': + dependencies: + '@polkadot-api/substrate-bindings': 0.11.1 + '@polkadot-api/utils': 0.1.2 + + '@polkadot-api/metadata-builders@0.13.0': + dependencies: + '@polkadot-api/substrate-bindings': 0.15.0 + '@polkadot-api/utils': 0.2.0 + + '@polkadot-api/observable-client@0.8.6(@polkadot-api/substrate-client@0.3.0)(rxjs@7.8.1)': + dependencies: + '@polkadot-api/metadata-builders': 0.10.2 + '@polkadot-api/substrate-bindings': 0.11.1 + '@polkadot-api/substrate-client': 0.3.0 + '@polkadot-api/utils': 0.1.2 + rxjs: 7.8.1 + + '@polkadot-api/substrate-bindings@0.11.1': + dependencies: + '@noble/hashes': 1.8.0 + '@polkadot-api/utils': 0.1.2 + '@scure/base': 1.2.6 + scale-ts: 1.6.1 + + '@polkadot-api/substrate-bindings@0.15.0': + dependencies: + '@noble/hashes': 1.8.0 + '@polkadot-api/utils': 0.2.0 + '@scure/base': 1.2.6 + scale-ts: 1.6.1 + + '@polkadot-api/substrate-client@0.3.0': + dependencies: + '@polkadot-api/json-rpc-provider': 0.0.4 + '@polkadot-api/utils': 0.1.2 + '@polkadot-api/utils@0.1.2': {} + '@polkadot-api/utils@0.2.0': {} + '@polkadot-labs/hdkd-helpers@0.0.20': dependencies: '@noble/curves': 1.9.6 @@ -3602,6 +3785,105 @@ snapshots: '@scure/sr25519': 0.2.0 scale-ts: 1.6.1 + '@polkadot/networks@13.5.5': + dependencies: + '@polkadot/util': 13.5.5 + '@substrate/ss58-registry': 1.51.0 + tslib: 2.8.1 + + '@polkadot/util-crypto@13.5.5(@polkadot/util@13.5.5)': + dependencies: + '@noble/curves': 1.9.6 + '@noble/hashes': 1.8.0 + '@polkadot/networks': 13.5.5 + '@polkadot/util': 13.5.5 + '@polkadot/wasm-crypto': 7.4.1(@polkadot/util@13.5.5)(@polkadot/x-randomvalues@13.5.5(@polkadot/util@13.5.5)(@polkadot/wasm-util@7.4.1(@polkadot/util@13.5.5))) + '@polkadot/wasm-util': 7.4.1(@polkadot/util@13.5.5) + '@polkadot/x-bigint': 13.5.5 + '@polkadot/x-randomvalues': 13.5.5(@polkadot/util@13.5.5)(@polkadot/wasm-util@7.4.1(@polkadot/util@13.5.5)) + '@scure/base': 1.2.6 + tslib: 2.8.1 + + '@polkadot/util@13.5.5': + dependencies: + '@polkadot/x-bigint': 13.5.5 + '@polkadot/x-global': 13.5.5 + '@polkadot/x-textdecoder': 13.5.5 + '@polkadot/x-textencoder': 13.5.5 + '@types/bn.js': 5.2.0 + bn.js: 5.2.2 + tslib: 2.8.1 + + '@polkadot/wasm-bridge@7.4.1(@polkadot/util@13.5.5)(@polkadot/x-randomvalues@13.5.5(@polkadot/util@13.5.5)(@polkadot/wasm-util@7.4.1(@polkadot/util@13.5.5)))': + dependencies: + '@polkadot/util': 13.5.5 + '@polkadot/wasm-util': 7.4.1(@polkadot/util@13.5.5) + '@polkadot/x-randomvalues': 13.5.5(@polkadot/util@13.5.5)(@polkadot/wasm-util@7.4.1(@polkadot/util@13.5.5)) + tslib: 2.8.1 + + '@polkadot/wasm-crypto-asmjs@7.4.1(@polkadot/util@13.5.5)': + dependencies: + '@polkadot/util': 13.5.5 + tslib: 2.8.1 + + '@polkadot/wasm-crypto-init@7.4.1(@polkadot/util@13.5.5)(@polkadot/x-randomvalues@13.5.5(@polkadot/util@13.5.5)(@polkadot/wasm-util@7.4.1(@polkadot/util@13.5.5)))': + dependencies: + '@polkadot/util': 13.5.5 + '@polkadot/wasm-bridge': 7.4.1(@polkadot/util@13.5.5)(@polkadot/x-randomvalues@13.5.5(@polkadot/util@13.5.5)(@polkadot/wasm-util@7.4.1(@polkadot/util@13.5.5))) + '@polkadot/wasm-crypto-asmjs': 7.4.1(@polkadot/util@13.5.5) + '@polkadot/wasm-crypto-wasm': 7.4.1(@polkadot/util@13.5.5) + '@polkadot/wasm-util': 7.4.1(@polkadot/util@13.5.5) + '@polkadot/x-randomvalues': 13.5.5(@polkadot/util@13.5.5)(@polkadot/wasm-util@7.4.1(@polkadot/util@13.5.5)) + tslib: 2.8.1 + + '@polkadot/wasm-crypto-wasm@7.4.1(@polkadot/util@13.5.5)': + dependencies: + '@polkadot/util': 13.5.5 + '@polkadot/wasm-util': 7.4.1(@polkadot/util@13.5.5) + tslib: 2.8.1 + + '@polkadot/wasm-crypto@7.4.1(@polkadot/util@13.5.5)(@polkadot/x-randomvalues@13.5.5(@polkadot/util@13.5.5)(@polkadot/wasm-util@7.4.1(@polkadot/util@13.5.5)))': + dependencies: + '@polkadot/util': 13.5.5 + '@polkadot/wasm-bridge': 7.4.1(@polkadot/util@13.5.5)(@polkadot/x-randomvalues@13.5.5(@polkadot/util@13.5.5)(@polkadot/wasm-util@7.4.1(@polkadot/util@13.5.5))) + '@polkadot/wasm-crypto-asmjs': 7.4.1(@polkadot/util@13.5.5) + '@polkadot/wasm-crypto-init': 7.4.1(@polkadot/util@13.5.5)(@polkadot/x-randomvalues@13.5.5(@polkadot/util@13.5.5)(@polkadot/wasm-util@7.4.1(@polkadot/util@13.5.5))) + '@polkadot/wasm-crypto-wasm': 7.4.1(@polkadot/util@13.5.5) + '@polkadot/wasm-util': 7.4.1(@polkadot/util@13.5.5) + '@polkadot/x-randomvalues': 13.5.5(@polkadot/util@13.5.5)(@polkadot/wasm-util@7.4.1(@polkadot/util@13.5.5)) + tslib: 2.8.1 + + '@polkadot/wasm-util@7.4.1(@polkadot/util@13.5.5)': + dependencies: + '@polkadot/util': 13.5.5 + tslib: 2.8.1 + + '@polkadot/x-bigint@13.5.5': + dependencies: + '@polkadot/x-global': 13.5.5 + tslib: 2.8.1 + + '@polkadot/x-global@13.5.5': + dependencies: + tslib: 2.8.1 + + '@polkadot/x-randomvalues@13.5.5(@polkadot/util@13.5.5)(@polkadot/wasm-util@7.4.1(@polkadot/util@13.5.5))': + dependencies: + '@polkadot/util': 13.5.5 + '@polkadot/wasm-util': 7.4.1(@polkadot/util@13.5.5) + '@polkadot/x-global': 13.5.5 + tslib: 2.8.1 + + '@polkadot/x-textdecoder@13.5.5': + dependencies: + '@polkadot/x-global': 13.5.5 + tslib: 2.8.1 + + '@polkadot/x-textencoder@13.5.5': + dependencies: + '@polkadot/x-global': 13.5.5 + tslib: 2.8.1 + '@popperjs/core@2.11.8': {} '@reown/appkit-common@1.7.8(bufferutil@4.0.9)(utf-8-validate@5.0.10)(zod@3.22.4)': @@ -3979,6 +4261,14 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} + '@substrate/connect-discovery@0.2.2': + dependencies: + '@substrate/discovery': 0.2.2 + + '@substrate/discovery@0.2.2': {} + + '@substrate/ss58-registry@1.51.0': {} + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.26.9)': dependencies: '@babel/core': 7.26.9 @@ -4085,6 +4375,10 @@ snapshots: dependencies: '@babel/types': 7.26.9 + '@types/bn.js@5.2.0': + dependencies: + '@types/node': 22.7.5 + '@types/cookie@0.6.0': {} '@types/debug@4.1.12': @@ -4100,7 +4394,6 @@ snapshots: '@types/node@22.7.5': dependencies: undici-types: 6.19.8 - optional: true '@types/parse-json@4.0.2': {} @@ -4828,6 +5121,8 @@ snapshots: dependencies: readdirp: 4.1.2 + client-only@0.0.1: {} + cliui@6.0.0: dependencies: string-width: 4.2.3 @@ -4968,7 +5263,7 @@ snapshots: dependencies: '@ecies/ciphers': 0.2.3(@noble/ciphers@1.3.0) '@noble/ciphers': 1.3.0 - '@noble/curves': 1.9.2 + '@noble/curves': 1.9.6 '@noble/hashes': 1.8.0 electron-to-chromium@1.5.113: {} @@ -5512,7 +5807,7 @@ snapshots: ox@0.6.7(zod@3.22.4): dependencies: '@adraffy/ens-normalize': 1.11.0 - '@noble/curves': 1.9.2 + '@noble/curves': 1.9.6 '@noble/hashes': 1.8.0 '@scure/bip32': 1.6.2 '@scure/bip39': 1.5.4 @@ -5775,7 +6070,7 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.34.9 fsevents: 2.3.3 - rxjs@7.8.2: + rxjs@7.8.1: dependencies: tslib: 2.8.1 @@ -5857,7 +6152,7 @@ snapshots: buffer-builder: 0.2.0 colorjs.io: 0.5.2 immutable: 5.0.3 - rxjs: 7.8.2 + rxjs: 7.8.1 supports-color: 8.1.1 sync-child-process: 1.0.2 varint: 6.0.0 @@ -5994,6 +6289,12 @@ snapshots: svg-parser@2.0.4: {} + swr@2.2.5(react@19.0.0): + dependencies: + client-only: 0.0.1 + react: 19.0.0 + use-sync-external-store: 1.4.0(react@19.0.0) + sync-child-process@1.0.2: dependencies: sync-message-port: 1.1.3 @@ -6024,8 +6325,7 @@ snapshots: uncrypto@0.1.3: {} - undici-types@6.19.8: - optional: true + undici-types@6.19.8: {} unstorage@1.16.0(idb-keyval@6.2.2): dependencies: diff --git a/src/App.jsx b/src/App.jsx index 7a17edf..c199696 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -16,6 +16,7 @@ import TopBar from "./components/TopBar/TopBar"; import { shouldTriggerSafetyCheck } from "./helpers"; import { isNetworkAvailable } from "./constants"; import useTheme from "./hooks/useTheme"; +import { useUnstableProvider } from "./hooks/ghost"; import { dark as darkTheme } from "./themes/dark.js"; import { girth as gTheme } from "./themes/girth.js"; import { light as lightTheme } from "./themes/light.js"; @@ -112,6 +113,24 @@ function App() { const isSmallerScreen = useMediaQuery("(max-width: 1047px)"); const isSmallScreen = useMediaQuery("(max-width: 600px)"); + const { + providerDetail, + providerDetails, + connectProviderDetail + } = useUnstableProvider() + + useEffect(() => { + // TODO: make sure we are using correct extension + const maybeProvider = providerDetails?.find(obj => obj.info.rdns === "io.ghostchain.GhostWalletExtension") + if (maybeProvider && !providerDetail) { + try { + connectProviderDetail(maybeProvider) + } catch (e) { + console.log(e) + } + } + }, [providerDetail, providerDetails, connectProviderDetail]) + useEffect(() => { if (shouldTriggerSafetyCheck()) { toast.success("Safety Check: Always verify you're on app.dao.ghostchain.io!", { duration: 5000 }); diff --git a/src/containers/Bridge/Bridge.jsx b/src/containers/Bridge/Bridge.jsx index f550f69..285a9f0 100644 --- a/src/containers/Bridge/Bridge.jsx +++ b/src/containers/Bridge/Bridge.jsx @@ -14,12 +14,16 @@ import { useMediaQuery, useTheme } from "@mui/material"; -import { ss58Decode } from "@polkadot-labs/hdkd-helpers"; +import { ss58Decode, ss58Address } from "@polkadot-labs/hdkd-helpers"; import { toHex } from "@polkadot-api/utils"; +import { decodeAddress } from "@polkadot/util-crypto"; import { useBlockNumber, useTransactionConfirmations } from "wagmi"; +import { keccak256 } from "viem"; +import { u64, u128 } from "scale-ts"; +import PendingIcon from '@mui/icons-material/Pending'; import PendingActionsIcon from '@mui/icons-material/PendingActions'; -import PublicIcon from '@mui/icons-material/Public'; +import ArrowBack from '@mui/icons-material/ArrowBack'; import ArrowRightIcon from '@mui/icons-material/ArrowRight'; import HourglassBottomIcon from '@mui/icons-material/HourglassBottom'; import ThumbUpIcon from '@mui/icons-material/ThumbUp'; @@ -42,6 +46,17 @@ import { formatCurrency } from "../../helpers"; import { useTokenSymbol, useBalance } from "../../hooks/tokens"; import { useGatekeeperAddress, ghost } from "../../hooks/staking"; +import { + useEvmNetwork, + useClapsInSession, + useApplauseThreshold, + useReceivedClaps, + useApplausesForTransaction, + useAuthorities, + useCurrentIndex, + useUnstableProvider, + useMetadata +} from "../../hooks/ghost"; const STORAGE_PREFIX = "storedTransactions" @@ -54,129 +69,79 @@ const Bridge = ({ chainId, address, config, connect }) => { const [isPending, setIsPending] = useState(false); const [bridgeAction, setBridgeAction] = useState(true); const [activeTxIndex, setActiveTxIndex] = useState(-1); - const [txStep, setTxStep] = useState(0); const [receiver, setReceiver] = useState(""); const [convertedReceiver, setConvertedReceiver] = useState(undefined); const [amount, setAmount] = useState(""); - // ReceivedClaps && ApplausesForTransaction - // session_index - // transaction_hash - // keccak256(receiver, amount, chain_id) + const sliceString = (string, first, second) => { + return string.slice(0, first) + "..." + string.slice(second); + } - // const initialStoredTransactions = sessionStorage.getItem(STORAGE_PREFIX); - const initialStoredTransactions = JSON.stringify([ - { - sessionIndex: 23, - transactionHash: "0x11111111111111111", - receiver: "sfAasdadasasads", - amount: "2312323232223232", - chainId: 11155111, - timestamp: Date.now() - }, - { - sessionIndex: 23, - transactionHash: "0x2222222222222222222", - receiver: "sfAasdadasasads", - amount: "1122232232", - chainId: 1, - timestamp: Date.now() - }, - { - sessionIndex: 24, - transactionHash: "0x333333333333333333", - receiver: "sfAasdadasasads", - amount: "99999999999999992", - chainId: 11155111, - timestamp: Date.now() - }, - { - sessionIndex: 23, - transactionHash: "0x4444444444444444444", - receiver: "sfAasdadasasads", - amount: "2312323232223232", - chainId: 11155111, - timestamp: Date.now() - }, - { - sessionIndex: 23, - transactionHash: "0x555555555555555555555", - receiver: "sfAasdadasasads", - amount: "1122232232", - chainId: 1, - timestamp: Date.now() - }, - { - sessionIndex: 24, - transactionHash: "0x66666666666666666666666666", - receiver: "sfAasdadasasads", - amount: "99999999999999992", - chainId: 11155111, - timestamp: Date.now() - }, - { - sessionIndex: 23, - transactionHash: "0x77777777777777777777777777", - receiver: "sfAasdadasasads", - amount: "2312323232223232", - chainId: 11155111, - timestamp: Date.now() - }, - { - sessionIndex: 23, - transactionHash: "0x888888888888888888888888888", - receiver: "sfAasdadasasads", - amount: "1122232232", - chainId: 1, - timestamp: Date.now() - }, - { - sessionIndex: 24, - transactionHash: "0x999999999999999999999", - receiver: "sfAasdadasasads", - amount: "99999999999999992", - chainId: 11155111, - timestamp: Date.now() - }, - { - sessionIndex: 23, - transactionHash: "0x10101010101010101010", - receiver: "sfAasdadasasads", - amount: "2312323232223232", - chainId: 11155111, - timestamp: Date.now() - }, - { - sessionIndex: 23, - transactionHash: "0x12121212121212212", - receiver: "sfAasdadasasads", - amount: "1122232232", - chainId: 1, - timestamp: Date.now() - }, - { - sessionIndex: 24, - transactionHash: "0x1313131313131313131", - receiver: "sfAasdadasasads", - amount: "99999999999999992", - chainId: 11155111, - timestamp: Date.now() - } - ]); + const initialStoredTransactions = sessionStorage.getItem(STORAGE_PREFIX); + // const initialStoredTransactions = JSON.stringify([ + // { + // sessionIndex: 124, + // transactionHash: "0x637276eccfa0787de396877a1a259964334fb52cb5111ea84bb28f0006f06276", + // receiverAddress: "sfK147dy2NapxEKwrTLLxTkmhw15kkoJeEKrg77oLFRmUQZDb", + // amount: "10000000000000000000", + // chainId: 11155111, + // blockNumber: 9033063, + // timestamp: Date.now() + // } + // ]); const [storedTransactions, setStoredTransactions] = useState( initialStoredTransactions ? JSON.parse(initialStoredTransactions) : [] ); - const incomingCommission = new DecimalBigNumber(69n, 100); - const validators = ["first", "second", "third"]; - const clappedValidators = 1; + const { providerDetail } = useUnstableProvider(); + const metadata = useMetadata(); + + const watchTransaction = useMemo(() => { + if (activeTxIndex < 0 || activeTxIndex >= storedTransactions?.length) { + return undefined + } + return storedTransactions?.at(activeTxIndex) + }, [activeTxIndex, storedTransactions]) + + const hashedArguments = useMemo(() => { + if (!watchTransaction) return undefined + + const amountEncoded = u128.enc(BigInt(watchTransaction.amount)); + const networkIdEncoded = u64.enc(BigInt(chainId)); + const addressEncoded = decodeAddress(watchTransaction.receiverAddress, false, 1996); + + const clapArgsStr = new Uint8Array([ + ...addressEncoded, + ...amountEncoded, + ...networkIdEncoded + ]); + return keccak256(clapArgsStr) + }, [watchTransaction]) + + const currentSession = useCurrentIndex(); + const evmNetwork = useEvmNetwork({ evmChainId: chainId }); + const authorities = useAuthorities({ + currentSession: watchTransaction?.sessionIndex ?? currentSession + }); + const clapsInSession = useClapsInSession({ + currentSession: watchTransaction?.sessionIndex ?? currentSession + }); + const appluseThreshold = useApplauseThreshold(); + const receivedClaps = useReceivedClaps({ + currentSession: watchTransaction?.sessionIndex ?? currentSession, + txHash: watchTransaction?.transactionHash, + argsHash: hashedArguments + }); + const transactionApplaused = useApplausesForTransaction({ + currentSession: watchTransaction?.sessionIndex ?? currentSession, + txHash: watchTransaction?.transactionHash, + argsHash: hashedArguments + }); + + const finalityDelay = Number(evmNetwork?.finality_delay ?? 0n); + const incomingFee = Number(evmNetwork?.incoming_fee ?? 0n) / 10000000; const { data: blockNumber } = useBlockNumber({ watch: true }); - // const { data: txtx } = useTransactionConfirmations({ - // hash: "0xdb30adfa3bfc58539bc3a9a92f0dcace8f251af90f8a4f525b57d95d28103afc", - // refetchInterval: 5000 - // }); - // console.log(txtx) const { gatekeeperAddress } = useGatekeeperAddress(chainId); const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST"); const { @@ -196,6 +161,11 @@ const Bridge = ({ chainId, address, config, connect }) => { } }, [receiver]) + const clapsInSessionLength = useMemo(() => { + const disabledIndexes = new Set(clapsInSession?.filter(item => item.at(1).disabled).map(item => item.at(0))); + return authorities?.filter((_, idx) => !disabledIndexes.has(idx)).length ?? 0; + }, [authorities, clapsInSession]); + const chainExplorerUrl = useMemo(() => { const client = config?.getClient(); return client?.chain?.blockExplorers?.default?.url; @@ -208,10 +178,36 @@ const Bridge = ({ chainId, address, config, connect }) => { const currentRecord = useMemo(() => { if (activeTxIndex === -1) return undefined - return storedTransactions.at(activeTxIndex) - }, [activeTxIndex, storedTransactions]); + const current = storedTransactions.at(activeTxIndex) + if (!current) return undefined - const gatekeeperAddressEmpty = useMemo(() => { + const finalization = Math.max(0, (finalityDelay + watchTransaction.blockNumber) - Number(blockNumber)); + const receivedClapsLength = receivedClaps?.length ?? 0; + const clapsNeeded = Math.floor(clapsInSessionLength * appluseThreshold / 100); + const step = finalization > 0 + ? 0 + : receivedClapsLength < clapsNeeded + ? 1 + : !transactionApplaused + ? 2 + : 3; + + return { + ...watchTransaction, + finalization, + step, + } + }, [ + transactionApplaused, + receivedClaps, + appluseThreshold, + clapsInSessionLength, + finalityDelay, + watchTransaction, + blockNumber + ]); + + const gatekeeperAddressEmpty = useMemo(() => { if (gatekeeperAddress === "0x0000000000000000000000000000000000000000") { return true; } @@ -241,10 +237,6 @@ const Bridge = ({ chainId, address, config, connect }) => { setActiveTxIndex(-1); }, [storedTransactions, activeTxIndex, setStoredTransactions, setActiveTxIndex]); - const handleMouseEnter = (index) => { - setTxStep(index); - } - const ghostOrConnect = async () => { if (address === "") { connect(); @@ -257,6 +249,21 @@ const Bridge = ({ chainId, address, config, connect }) => { setReceiver(""); setAmount(""); setIsPending(false); + + const transaction = { + sessionIndex: currentSession ?? 0, + transactionHash: txHash, + receiverAddress: receiver, + amount: preparedAmount.toString(), + chainId: chainId, + blockNumber: Number(blockNumber), + timestamp: Date.now() + } + + const newStoredTransactions = [...storedTransactions, transaction]; + setStoredTransactions(newStoredTransactions); + sessionStorage.setItem(STORAGE_PREFIX, JSON.stringify(newStoredTransactions)); + setActiveTxIndex(newStoredTransactions.length - 1) } } @@ -297,16 +304,16 @@ const Bridge = ({ chainId, address, config, connect }) => { target="_blank" rel="noopener noreferrer" href={currentRecord - ? `${chainExplorerUrl}/tx/${currentRecord.transactionHash}` + ? `${chainExplorerUrl}/tx/${currentRecord ? currentRecord.transactionHash : ""}` : "" } > - {currentRecord?.transactionHash.slice(0, 9)}...{currentRecord?.transactionHash.slice(-9)} + {currentRecord ? sliceString(currentRecord.transactionHash, 10, -8) : ""} } - open={activeTxIndex > -1} + open={activeTxIndex >= 0} onClose={() => setActiveTxIndex(-1)} minHeight={"100px"} > @@ -315,15 +322,14 @@ const Bridge = ({ chainId, address, config, connect }) => { 0 && theme.colors.primary[300] }} width="120px" display="flex" flexDirection="column" justifyContent="start" alignItems="center" - onMouseEnter={() => handleMouseEnter(0)} > { component={HourglassBottomIcon} /> Finalization - {blockNumber?.toString()} blocks left + + {Math.max( + 0, + (finalityDelay + (currentRecord ? currentRecord.blockNumber : 0) - Number(blockNumber)) + ).toString()} blocks left + 1 && theme.colors.primary[300] }} width="120px" display="flex" flexDirection="column" justifyContent="start" alignItems="center" - onMouseEnter={() => handleMouseEnter(1)} > { /> - Slow claps - {clappedValidators} / {validators.length} + Slow Claps + {receivedClaps?.length ?? 0} / {clapsInSessionLength} = 2 && "scale(1.2)", + color: currentRecord?.step >= 2 && theme.colors.primary[300] }} width="120px" display="flex" flexDirection="column" justifyContent="start" alignItems="center" - onMouseEnter={() => handleMouseEnter(2)} > Applaused - Check Receiver + { + currentRecord?.step === 3 ? "Check Receiver" : "Waiting Room" + } @@ -405,9 +419,20 @@ const Bridge = ({ chainId, address, config, connect }) => { Session Index: {currentRecord?.sessionIndex} - + Receiver Address: - {currentRecord?.receiver} + navigator.clipboard.writeText(currentRecord ? currentRecord.receiverAddress : "")} + > + { + currentRecord ? sliceString(currentRecord.receiverAddress, 14, -5) : "" + } + Sent Amount: @@ -424,13 +449,33 @@ const Bridge = ({ chainId, address, config, connect }) => { new Date(currentRecord ? currentRecord.timestamp : 0).toLocaleString('en-US') } +
+ + Transaction Hash: + navigator.clipboard.writeText(currentRecord ? currentRecord.transactionHash : "")} + > + { + currentRecord ? sliceString(currentRecord.transactionHash, 10, -8) : "0x" + } + + + + Arguments Hash: + navigator.clipboard.writeText(hashedArguments ? hashedArguments : "")} + > + { + hashedArguments ? sliceString(hashedArguments, 10, -8) : "0x" + } + +
removeStoredRecord()} > @@ -446,22 +491,24 @@ const Bridge = ({ chainId, address, config, connect }) => { + + {!bridgeAction && ( setBridgeAction(!bridgeAction)} + />)} { - bridgeAction - ? `Bridge $${ghstSymbol}` - : "Transaction history" + bridgeAction ? `Bridge $${ghstSymbol}` : "Transaction history" } } - topRight={ - setBridgeAction(!bridgeAction)} - /> - } + topRight={bridgeAction && ( setBridgeAction(!bridgeAction)} + />)} enableBackground fullWidth @@ -504,7 +551,7 @@ const Bridge = ({ chainId, address, config, connect }) => { - There is no connected gatekeeper on {chainName} network. Propose gatekeeper on this network to make validators listen to it. + There is no connected gatekeeper on {chainName} network. Propose gatekeeper on this network to make authorities listen to it. @@ -519,40 +566,55 @@ const Bridge = ({ chainId, address, config, connect }) => { rel="noopener noreferrer" href={`${chainExplorerUrl}/token/${gatekeeperAddress}`} > - {gatekeeperAddress.slice(0, 10) + "..." + gatekeeperAddress.slice(-8)} + {sliceString(gatekeeperAddress, 10, -8)} )} - {incomingCommission && validators?.length ? ( - - - - GHOST Wallet is not detected on your browser. Download  - - GHOST Wallet -   to see full detalization for bridge transaction. - - - - ) - : ( - - - {!isVerySmallScreen && Est. Commission:} - unknown - - - {!isVerySmallScreen && Number of validators:} - unknown - - - )} - + + {!providerDetail + ? ( + + + GHOST Wallet is not detected on your browser. Download  + + GHOST Wallet +   to see full detalization for bridge transaction. + + + ) + : metadata + ? ( + + + {!isVerySmallScreen && Est. Commission:} + {incomingFee.toFixed(4)}% + + + {!isVerySmallScreen && Finality Delay:} + {finalityDelay} blocks + + + {!isVerySmallScreen && Current Epoch:} + {currentSession ?? 0} + + + {!isVerySmallScreen && Number of validators:} + {clapsInSessionLength} + + + ) + : ( + + Downloading chain metadata, wait please... + + ) + } + { )} {!bridgeAction && ( - + {!isSemiSmallScreen && ( Amount Datetime Status - + )} {filteredStoredTransactions .map((obj, idx) => ( { onClick={() => setActiveTxIndex(idx)} > - + {formatCurrency( new DecimalBigNumber(BigInt(obj.amount), 18).toString(), - 3, + isSemiSmallScreen ? 3 : 8, ghstSymbol )} + + {sliceString( + obj.receiverAddress, + isSemiSmallScreen ? 5 : 10, + isSemiSmallScreen ? -3 : -8 + )} + - - + + {new Date(obj.timestamp).toLocaleDateString('en-US')} - + {new Date(obj.timestamp).toLocaleTimeString('en-US')} @@ -626,24 +696,24 @@ const Bridge = ({ chainId, address, config, connect }) => { justifyContent="center" alignItems="center" sx={{ - width: "25px", - height: "25px", - background: idx % 2 === 0 + width: "20px", + height: "20px", + background: Number(blockNumber) - (obj.blockNumber + finalityDelay) < 0 ? theme.colors.feedback.warning : theme.colors.feedback.success, borderRadius: "100%", boxShadow: "0px 0px 1px black" }} > - {idx % 2 === 0 ? + {Number(blockNumber) - (obj.blockNumber + finalityDelay) < 0 ? : diff --git a/src/hooks/ghost/MetadataProvider.jsx b/src/hooks/ghost/MetadataProvider.jsx new file mode 100644 index 0000000..29b5838 --- /dev/null +++ b/src/hooks/ghost/MetadataProvider.jsx @@ -0,0 +1,44 @@ +import React, { ReactNode } from "react" +import useSWR from "swr" +import { createContext, useContext } from 'react'; + +import { decAnyMetadata, unifyMetadata } from "@polkadot-api/substrate-bindings" +import { useUnstableProvider } from "./UnstableProvider" + +const MetadataProvider = createContext(null) +export const useMetadata = () => useContext(MetadataProvider) + +export const MetadataProviderProvider = ({ children }) => { + const { client, chainId } = useUnstableProvider() + const { data: metadata } = useSWR( + client && chainId ? ["metadata", client, chainId] : null, + async ([_, client]) => { + const storageKey = `metadata-${chainId}` + const storedMetadata = sessionStorage.getItem(storageKey) + if (storedMetadata) return unifyMetadata(decAnyMetadata(storedMetadata)) + + const metadata = await new Promise((resolve, reject) => + client._request("state_getMetadata", [], { + onSuccess: resolve, + onError: reject, + }), + ) + .then(r => r) + .catch(_ => undefined) + + if (metadata) { + sessionStorage.setItem(storageKey, metadata) + return unifyMetadata(decAnyMetadata(metadata)) + } else { + sessionStorage.removeItem(storageKey) + return undefined + } + } + ) + + return ( + + {children} + + ) +} diff --git a/src/hooks/ghost/UnstableProvider.jsx b/src/hooks/ghost/UnstableProvider.jsx new file mode 100644 index 0000000..3ceb85c --- /dev/null +++ b/src/hooks/ghost/UnstableProvider.jsx @@ -0,0 +1,55 @@ +import { createContext, useContext, useState, useMemo } from "react" +import { Unstable } from "@substrate/connect-discovery" +import { createClient } from "@polkadot-api/substrate-client" +import { getObservableClient } from "@polkadot-api/observable-client" +import useSWR from "swr" + +const DEFAULT_CHAIN_ID = "0xa217f4ee58a944470e9633ca5bd6d28a428ed64cd9b6f3e413565f359f89af90" +const UnstableProvider = createContext(null) +export const useUnstableProvider = () => useContext(UnstableProvider) + +export const UnstableProviderProvider = ({ children }) => { + const { data: providerDetails } = useSWR("getGhostProviders", () => + Unstable.getSubstrateConnectExtensionProviders() + ); + + const [providerDetail, setProviderDetail] = useState(); + const { data: provider } = useSWR( + () => providerDetail ? `ghostProviderDetail.${providerDetail.info.uuid}.provider` : null, + () => providerDetail ? providerDetail.provider : null + ); + + const [chainId, setChainId] = useState(DEFAULT_CHAIN_ID); + + const client = useMemo(() => { + if (!provider || !chainId) return undefined; + const chain = provider.getChains()[chainId]; + if (!chain) return undefined; + return createClient(chain.connect); + }, [provider, chainId]); + + const observableClient = useMemo(() => { + return client ? getObservableClient(client) : undefined; + }, [client]); + + const chainHead$ = useMemo(() => { + return observableClient ? observableClient.chainHead$() : undefined; + }, [observableClient]); + + return ( + + {children} + + ); +}; diff --git a/src/hooks/ghost/index.js b/src/hooks/ghost/index.js new file mode 100644 index 0000000..da4f253 --- /dev/null +++ b/src/hooks/ghost/index.js @@ -0,0 +1,9 @@ +export * from "./UnstableProvider"; +export * from "./MetadataProvider"; +export * from "./useCurrentIndex"; +export * from "./useEvmNetwork"; +export * from "./useClapsInSession"; +export * from "./useApplauseThreshold"; +export * from "./useReceivedClaps"; +export * from "./useAuthorities"; +export * from "./useApplausesForTransaction"; diff --git a/src/hooks/ghost/useApplauseThreshold.js b/src/hooks/ghost/useApplauseThreshold.js new file mode 100644 index 0000000..7b4f5a0 --- /dev/null +++ b/src/hooks/ghost/useApplauseThreshold.js @@ -0,0 +1,23 @@ +import useSWR from "swr" +import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders" + +import { useUnstableProvider } from "./UnstableProvider" +import { useMetadata } from "./MetadataProvider" + +export const useApplauseThreshold = () => { + const { chainId } = useUnstableProvider() + const metadata = useMetadata() + const { data: existentialDeposit } = useSWR( + chainId && metadata + ? ["ApplauseThreshold", chainId, metadata] + : null, + ([_, chainId, metadata]) => { + const builder = getDynamicBuilder(getLookupFn(metadata)) + const codec = builder.buildConstant("GhostSlowClaps", "ApplauseThreshold") + const constants = metadata?.pallets?.find(obj => obj.name === "GhostSlowClaps")?.constants + const value = constants?.find(obj => obj.name === "ApplauseThreshold")?.value + return value ? codec.dec(value) : undefined + } + ) + return existentialDeposit +} diff --git a/src/hooks/ghost/useApplausesForTransaction.js b/src/hooks/ghost/useApplausesForTransaction.js new file mode 100644 index 0000000..f956581 --- /dev/null +++ b/src/hooks/ghost/useApplausesForTransaction.js @@ -0,0 +1,47 @@ +import useSWRSubscription from "swr/subscription" +import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders" +import { distinct, filter, map, mergeMap } from "rxjs" +import { fromHex } from "@polkadot-api/utils"; + +import { useUnstableProvider } from "./UnstableProvider" +import { useMetadata } from "./MetadataProvider" + +export const useApplausesForTransaction = ({ currentSession, txHash, argsHash }) => { + const { chainHead$, chainId } = useUnstableProvider() + const metadata = useMetadata() + const { data: applausesForTransaction } = useSWRSubscription( + chainHead$ && txHash && argsHash && currentSession && chainId && metadata + ? ["applausesForTransaction", chainHead$, txHash, argsHash, currentSession, chainId, metadata] + : null, + ([_, chainHead$, txHash, argsHash, currentSession, chainId, metadata], { next }) => { + const { finalized$, storage$ } = chainHead$ + const subscription = finalized$.pipe( + filter(Boolean), + mergeMap((blockInfo) => { + const builder = getDynamicBuilder(getLookupFn(metadata)) + const applausesForTransaction = builder.buildStorage("GhostSlowClaps", "ApplausesForTransaction") + + return storage$(blockInfo?.hash, "value", () => + applausesForTransaction?.keys.enc( + currentSession, + { asBytes: () => fromHex(txHash) }, + { asBytes: () => fromHex(argsHash) }, + ) + ).pipe( + filter(Boolean), + distinct(), + map((value) => applausesForTransaction?.value.dec(value)) + ) + }), + ) + .subscribe({ + next(applausesForTransaction) { + next(null, applausesForTransaction) + }, + error: next, + }) + return () => subscription.unsubscribe() + } + ) + return applausesForTransaction +} diff --git a/src/hooks/ghost/useAuthorities.js b/src/hooks/ghost/useAuthorities.js new file mode 100644 index 0000000..a11ecc4 --- /dev/null +++ b/src/hooks/ghost/useAuthorities.js @@ -0,0 +1,41 @@ +import useSWRSubscription from "swr/subscription" +import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders" +import { distinct, filter, map, mergeMap } from "rxjs" + +import { useUnstableProvider } from "./UnstableProvider" +import { useMetadata } from "./MetadataProvider" + +export const useAuthorities = ({ currentSession }) => { + const { chainHead$, chainId } = useUnstableProvider() + const metadata = useMetadata() + const { data: slowClapAuthorities } = useSWRSubscription( + chainHead$ && chainId && metadata + ? ["slowClapAuthorities", chainHead$, currentSession, chainId, metadata] + : null, + ([_, chainHead$, currentSession, chainId, metadata], { next }) => { + const { finalized$, storage$ } = chainHead$ + const subscription = finalized$.pipe( + filter(Boolean), + mergeMap((blockInfo) => { + const builder = getDynamicBuilder(getLookupFn(metadata)) + const slowClapAuthorities = builder.buildStorage("GhostSlowClaps", "Authorities") + return storage$(blockInfo?.hash, "value", () => + slowClapAuthorities?.keys.enc(currentSession) + ).pipe( + filter(Boolean), + distinct(), + map((value) => slowClapAuthorities?.value.dec(value)) + ) + }), + ) + .subscribe({ + next(slowClapAuthorities) { + next(null, slowClapAuthorities) + }, + error: next, + }) + return () => subscription.unsubscribe() + } + ) + return slowClapAuthorities +} diff --git a/src/hooks/ghost/useClapsInSession.js b/src/hooks/ghost/useClapsInSession.js new file mode 100644 index 0000000..afed77c --- /dev/null +++ b/src/hooks/ghost/useClapsInSession.js @@ -0,0 +1,41 @@ +import useSWRSubscription from "swr/subscription" +import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders" +import { distinct, filter, map, mergeMap } from "rxjs" + +import { useUnstableProvider } from "./UnstableProvider" +import { useMetadata } from "./MetadataProvider" + +export const useClapsInSession = ({ currentSession }) => { + const { chainHead$, chainId } = useUnstableProvider() + const metadata = useMetadata() + const { data: clapsInSession } = useSWRSubscription( + chainHead$ && chainId && metadata + ? ["clapsInSession", chainHead$, currentSession, chainId, metadata] + : null, + ([_, chainHead$, currentSession, chainId, metadata], { next }) => { + const { finalized$, storage$ } = chainHead$ + const subscription = finalized$.pipe( + filter(Boolean), + mergeMap((blockInfo) => { + const builder = getDynamicBuilder(getLookupFn(metadata)) + const clapsInSession = builder.buildStorage("GhostSlowClaps", "ClapsInSession") + return storage$(blockInfo?.hash, "value", () => + clapsInSession?.keys.enc(currentSession) + ).pipe( + filter(Boolean), + distinct(), + map((value) => clapsInSession?.value.dec(value)) + ) + }), + ) + .subscribe({ + next(clapsInSession) { + next(null, clapsInSession) + }, + error: next, + }) + return () => subscription.unsubscribe() + } + ) + return clapsInSession +} diff --git a/src/hooks/ghost/useCurrentIndex.js b/src/hooks/ghost/useCurrentIndex.js new file mode 100644 index 0000000..c043d3e --- /dev/null +++ b/src/hooks/ghost/useCurrentIndex.js @@ -0,0 +1,41 @@ +import useSWRSubscription from "swr/subscription" +import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders" +import { distinct, filter, map, mergeMap } from "rxjs" + +import { useUnstableProvider } from "./UnstableProvider" +import { useMetadata } from "./MetadataProvider" + +export const useCurrentIndex = () => { + const { chainHead$, chainId } = useUnstableProvider() + const metadata = useMetadata() + const { data: currentIndex } = useSWRSubscription( + chainHead$ && chainId && metadata + ? ["currentIndex", chainHead$, chainId, metadata] + : null, + ([_, chainHead$, chainId, metadata], { next }) => { + const { finalized$, storage$ } = chainHead$ + const subscription = finalized$.pipe( + filter(Boolean), + mergeMap((blockInfo) => { + const builder = getDynamicBuilder(getLookupFn(metadata)) + const currentIndex = builder.buildStorage("Session", "CurrentIndex") + return storage$(blockInfo?.hash, "value", () => + currentIndex?.keys.enc() + ).pipe( + filter(Boolean), + distinct(), + map((value) => currentIndex?.value.dec(value)) + ) + }), + ) + .subscribe({ + next(currentIndex) { + next(null, currentIndex) + }, + error: next, + }) + return () => subscription.unsubscribe() + } + ) + return currentIndex +} diff --git a/src/hooks/ghost/useEvmNetwork.js b/src/hooks/ghost/useEvmNetwork.js new file mode 100644 index 0000000..46047a8 --- /dev/null +++ b/src/hooks/ghost/useEvmNetwork.js @@ -0,0 +1,41 @@ +import useSWRSubscription from "swr/subscription" +import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders" +import { distinct, filter, map, mergeMap } from "rxjs" + +import { useUnstableProvider } from "./UnstableProvider" +import { useMetadata } from "./MetadataProvider" + +export const useEvmNetwork = ({ evmChainId }) => { + const { chainHead$, chainId } = useUnstableProvider() + const metadata = useMetadata() + const { data: evmNetwork } = useSWRSubscription( + chainHead$ && chainId && metadata + ? ["evmNetwork", chainHead$, evmChainId, chainId, metadata] + : null, + ([_, chainHead$, evmChainId, chainId, metadata], { next }) => { + const { finalized$, storage$ } = chainHead$ + const subscription = finalized$.pipe( + filter(Boolean), + mergeMap((blockInfo) => { + const builder = getDynamicBuilder(getLookupFn(metadata)) + const evmNetwork = builder.buildStorage("GhostNetworks", "Networks") + return storage$(blockInfo?.hash, "value", () => + evmNetwork?.keys.enc(BigInt(evmChainId)) + ).pipe( + filter(Boolean), + distinct(), + map((value) => evmNetwork?.value.dec(value)) + ) + }), + ) + .subscribe({ + next(evmNetwork) { + next(null, evmNetwork) + }, + error: next, + }) + return () => subscription.unsubscribe() + } + ) + return evmNetwork +} diff --git a/src/hooks/ghost/useReceivedClaps.js b/src/hooks/ghost/useReceivedClaps.js new file mode 100644 index 0000000..6f8ae00 --- /dev/null +++ b/src/hooks/ghost/useReceivedClaps.js @@ -0,0 +1,47 @@ +import useSWRSubscription from "swr/subscription" +import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders" +import { distinct, filter, map, mergeMap } from "rxjs" +import { fromHex } from "@polkadot-api/utils"; + +import { useUnstableProvider } from "./UnstableProvider" +import { useMetadata } from "./MetadataProvider" + +export const useReceivedClaps = ({ currentSession, txHash, argsHash }) => { + const { chainHead$, chainId } = useUnstableProvider() + const metadata = useMetadata() + const { data: receivedClaps } = useSWRSubscription( + chainHead$ && txHash && argsHash && currentSession && chainId && metadata + ? ["receivedClaps", chainHead$, txHash, argsHash, currentSession, chainId, metadata] + : null, + ([_, chainHead$, txHash, argsHash, currentSession, chainId, metadata], { next }) => { + const { finalized$, storage$ } = chainHead$ + const subscription = finalized$.pipe( + filter(Boolean), + mergeMap((blockInfo) => { + const builder = getDynamicBuilder(getLookupFn(metadata)) + const receivedClaps = builder.buildStorage("GhostSlowClaps", "ReceivedClaps") + + return storage$(blockInfo?.hash, "value", () => + receivedClaps?.keys.enc( + currentSession, + { asBytes: () => fromHex(txHash) }, + { asBytes: () => fromHex(argsHash) }, + ) + ).pipe( + filter(Boolean), + distinct(), + map((value) => receivedClaps?.value.dec(value)) + ) + }), + ) + .subscribe({ + next(receivedClaps) { + next(null, receivedClaps) + }, + error: next, + }) + return () => subscription.unsubscribe() + } + ) + return receivedClaps +} diff --git a/src/main.jsx b/src/main.jsx index dbbd427..a53193e 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -8,6 +8,7 @@ import { HashRouter } from "react-router-dom"; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { WagmiProvider } from "wagmi"; import { config } from "./config"; +import { UnstableProviderProvider, MetadataProviderProvider } from "./hooks/ghost" import ReactGA from "react-ga4"; const queryClient = new QueryClient(); @@ -21,7 +22,11 @@ ReactDOM.createRoot(document.getElementById('root')).render( - + + + + +