diff --git a/package-lock.json b/package-lock.json index 1f6b96fd..1260dacf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,6 +56,7 @@ "lucide-react": "^0.462.0", "marked": "^12.0.1", "next-themes": "^0.3.0", + "qrcode": "^1.5.4", "react": "^18.3.1", "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", @@ -75,6 +76,7 @@ "@eslint/js": "^9.9.0", "@tailwindcss/typography": "^0.5.16", "@types/node": "^22.5.5", + "@types/qrcode": "^1.5.6", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react-swc": "^3.5.0", @@ -3708,6 +3710,16 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/react": { "version": "18.3.26", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", @@ -4266,6 +4278,15 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -4361,6 +4382,72 @@ "url": "https://polar.sh/cva" } }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -4609,6 +4696,15 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decimal.js-light": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", @@ -4634,6 +4730,12 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "license": "Apache-2.0" }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -5160,6 +5262,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", @@ -5876,6 +5987,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -5899,7 +6019,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5972,6 +6091,15 @@ "node": ">= 6" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -6188,6 +6316,23 @@ "node": ">=6" } }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -6486,6 +6631,21 @@ "decimal.js-light": "^2.4.1" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -6629,6 +6789,12 @@ "node": ">=10" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -7299,6 +7465,12 @@ "node": ">= 8" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -7418,6 +7590,140 @@ } } }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index be7f724b..3ea3f319 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "lucide-react": "^0.462.0", "marked": "^12.0.1", "next-themes": "^0.3.0", + "qrcode": "^1.5.4", "react": "^18.3.1", "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", @@ -78,6 +79,7 @@ "@eslint/js": "^9.9.0", "@tailwindcss/typography": "^0.5.16", "@types/node": "^22.5.5", + "@types/qrcode": "^1.5.6", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react-swc": "^3.5.0", diff --git a/src/App.css b/src/App.css index 47581fa5..e73a2016 100644 --- a/src/App.css +++ b/src/App.css @@ -50,4 +50,4 @@ .read-the-docs { color: #5f7676; -} +} \ No newline at end of file diff --git a/src/components/AccountBalance.tsx b/src/components/AccountBalance.tsx index b81405ad..b390db6c 100644 --- a/src/components/AccountBalance.tsx +++ b/src/components/AccountBalance.tsx @@ -15,6 +15,7 @@ export const AccountBalance: React.FC = () => { reserved: '0', total: '0', }); + const [pezBalance, setPezBalance] = useState('0'); const [isLoading, setIsLoading] = useState(false); const fetchBalance = async () => { @@ -22,12 +23,13 @@ export const AccountBalance: React.FC = () => { setIsLoading(true); try { + // Fetch HEZ balance const { data: balanceData } = await api.query.system.account(selectedAccount.address); const free = balanceData.free.toString(); const reserved = balanceData.reserved.toString(); - // Convert from plancks to tokens (assuming 12 decimals like DOT) + // Convert from plancks to tokens (12 decimals) const decimals = 12; const divisor = Math.pow(10, decimals); @@ -40,6 +42,23 @@ export const AccountBalance: React.FC = () => { reserved: reservedTokens, total: totalTokens, }); + + // Fetch PEZ balance (Asset ID: 1) + try { + const pezAssetBalance = await api.query.assets.account(1, selectedAccount.address); + + if (pezAssetBalance.isSome) { + const assetData = pezAssetBalance.unwrap(); + const pezAmount = assetData.balance.toString(); + const pezTokens = (parseInt(pezAmount) / divisor).toFixed(4); + setPezBalance(pezTokens); + } else { + setPezBalance('0'); + } + } catch (error) { + console.error('Failed to fetch PEZ balance:', error); + setPezBalance('0'); + } } catch (error) { console.error('Failed to fetch balance:', error); } finally { @@ -50,13 +69,15 @@ export const AccountBalance: React.FC = () => { useEffect(() => { fetchBalance(); - // Subscribe to balance updates - let unsubscribe: () => void; + // Subscribe to HEZ balance updates + let unsubscribeHez: () => void; + let unsubscribePez: () => void; const subscribeBalance = async () => { if (!api || !isApiReady || !selectedAccount) return; - unsubscribe = await api.query.system.account( + // Subscribe to HEZ balance + unsubscribeHez = await api.query.system.account( selectedAccount.address, ({ data: balanceData }) => { const free = balanceData.free.toString(); @@ -76,12 +97,35 @@ export const AccountBalance: React.FC = () => { }); } ); + + // Subscribe to PEZ balance (Asset ID: 1) + try { + unsubscribePez = await api.query.assets.account( + 1, + selectedAccount.address, + (assetBalance) => { + if (assetBalance.isSome) { + const assetData = assetBalance.unwrap(); + const pezAmount = assetData.balance.toString(); + const decimals = 12; + const divisor = Math.pow(10, decimals); + const pezTokens = (parseInt(pezAmount) / divisor).toFixed(4); + setPezBalance(pezTokens); + } else { + setPezBalance('0'); + } + } + ); + } catch (error) { + console.error('Failed to subscribe to PEZ balance:', error); + } }; subscribeBalance(); return () => { - if (unsubscribe) unsubscribe(); + if (unsubscribeHez) unsubscribeHez(); + if (unsubscribePez) unsubscribePez(); }; }, [api, isApiReady, selectedAccount]); @@ -100,12 +144,12 @@ export const AccountBalance: React.FC = () => { return (
- {/* Total Balance Card */} + {/* HEZ Balance Card */}
- Total Balance + HEZ Balance +
+ + {/* Warning */} +
+

+ Important: Only send PezkuwiChain compatible tokens to this address. Sending other tokens may result in permanent loss. +

+
+
+ + + ); +}; \ No newline at end of file diff --git a/src/components/TransactionHistory.tsx b/src/components/TransactionHistory.tsx new file mode 100644 index 00000000..b40ca6b7 --- /dev/null +++ b/src/components/TransactionHistory.tsx @@ -0,0 +1,289 @@ +import React, { useEffect, useState } from 'react'; +import { usePolkadot } from '@/contexts/PolkadotContext'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { History, ExternalLink, ArrowUpRight, ArrowDownRight, RefreshCw } from 'lucide-react'; +import { useToast } from '@/hooks/use-toast'; + +interface TransactionHistoryProps { + isOpen: boolean; + onClose: () => void; +} + +interface Transaction { + blockNumber: number; + extrinsicIndex: number; + hash: string; + method: string; + section: string; + from: string; + to?: string; + amount?: string; + success: boolean; + timestamp?: number; +} + +export const TransactionHistory: React.FC = ({ isOpen, onClose }) => { + const { api, isApiReady, selectedAccount } = usePolkadot(); + const { toast } = useToast(); + const [transactions, setTransactions] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + const fetchTransactions = async () => { + if (!api || !isApiReady || !selectedAccount) return; + + setIsLoading(true); + try { + console.log('Fetching transactions...'); + const currentBlock = await api.rpc.chain.getBlock(); + const currentBlockNumber = currentBlock.block.header.number.toNumber(); + + console.log('Current block number:', currentBlockNumber); + + const txList: Transaction[] = []; + const blocksToCheck = Math.min(200, currentBlockNumber); + + for (let i = 0; i < blocksToCheck && txList.length < 20; i++) { + const blockNumber = currentBlockNumber - i; + + try { + const blockHash = await api.rpc.chain.getBlockHash(blockNumber); + const block = await api.rpc.chain.getBlock(blockHash); + + // Try to get timestamp, but don't fail if state is pruned + let timestamp = 0; + try { + const ts = await api.query.timestamp.now.at(blockHash); + timestamp = ts.toNumber(); + } catch (error) { + // State pruned, use current time as fallback + timestamp = Date.now(); + } + + console.log(`Block #${blockNumber}: ${block.block.extrinsics.length} extrinsics`); + + // Check each extrinsic in the block + block.block.extrinsics.forEach((extrinsic, index) => { + // Skip unsigned extrinsics (system calls) + if (!extrinsic.isSigned) { + return; + } + + const { method, signer } = extrinsic; + + console.log(` Extrinsic #${index}: ${method.section}.${method.method}, signer: ${signer.toString()}`); + + // Check if transaction involves our account + const fromAddress = signer.toString(); + const isFromOurAccount = fromAddress === selectedAccount.address; + + // Parse balances.transfer or balances.transferKeepAlive + if (method.section === 'balances' && + (method.method === 'transfer' || method.method === 'transferKeepAlive')) { + const [dest, value] = method.args; + const toAddress = dest.toString(); + const isToOurAccount = toAddress === selectedAccount.address; + + if (isFromOurAccount || isToOurAccount) { + txList.push({ + blockNumber, + extrinsicIndex: index, + hash: extrinsic.hash.toHex(), + method: method.method, + section: method.section, + from: fromAddress, + to: toAddress, + amount: value.toString(), + success: true, + timestamp: timestamp, + }); + } + } + + // Parse assets.transfer (PEZ, USDT, etc.) + if (method.section === 'assets' && method.method === 'transfer') { + const [assetId, dest, value] = method.args; + const toAddress = dest.toString(); + const isToOurAccount = toAddress === selectedAccount.address; + + if (isFromOurAccount || isToOurAccount) { + txList.push({ + blockNumber, + extrinsicIndex: index, + hash: extrinsic.hash.toHex(), + method: `${method.method} (Asset ${assetId.toString()})`, + section: method.section, + from: fromAddress, + to: toAddress, + amount: value.toString(), + success: true, + timestamp: timestamp, + }); + } + } + }); + } catch (blockError) { + console.warn(`Error processing block #${blockNumber}:`, blockError); + // Continue to next block + } + } + + console.log('Found transactions:', txList.length); + + setTransactions(txList); + } catch (error) { + console.error('Failed to fetch transactions:', error); + toast({ + title: "Error", + description: "Failed to fetch transaction history", + variant: "destructive", + }); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + if (isOpen) { + fetchTransactions(); + } + }, [isOpen, api, isApiReady, selectedAccount]); + + const formatAmount = (amount: string, decimals: number = 12) => { + const value = parseInt(amount) / Math.pow(10, decimals); + return value.toFixed(4); + }; + + const formatTimestamp = (timestamp?: number) => { + if (!timestamp) return 'Unknown'; + const date = new Date(timestamp); + return date.toLocaleString(); + }; + + const isIncoming = (tx: Transaction) => { + return tx.to === selectedAccount?.address; + }; + + return ( + + + +
+
+ Transaction History + + Recent transactions involving your account + +
+ +
+
+ +
+ {isLoading ? ( +
+ +

Loading transactions...

+
+ ) : transactions.length === 0 ? ( +
+ +

No transactions found

+

+ Your recent transactions will appear here +

+
+ ) : ( + transactions.map((tx, index) => ( +
+
+
+ {isIncoming(tx) ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+
+ {isIncoming(tx) ? 'Received' : 'Sent'} +
+
+ {tx.section}.{tx.method} +
+
+
+
+
+ {isIncoming(tx) ? '+' : '-'}{formatAmount(tx.amount || '0')} +
+
+ Block #{tx.blockNumber} +
+
+
+ +
+
+ From: +
+ {tx.from.slice(0, 8)}...{tx.from.slice(-6)} +
+
+ {tx.to && ( +
+ To: +
+ {tx.to.slice(0, 8)}...{tx.to.slice(-6)} +
+
+ )} +
+ +
+
+ {formatTimestamp(tx.timestamp)} +
+ +
+
+ )) + )} +
+
+
+ ); +}; \ No newline at end of file diff --git a/src/components/TransferModal.tsx b/src/components/TransferModal.tsx index a2a599a7..87dae011 100644 --- a/src/components/TransferModal.tsx +++ b/src/components/TransferModal.tsx @@ -10,6 +10,13 @@ import { import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; import { ArrowRight, Loader2, CheckCircle, XCircle } from 'lucide-react'; import { useToast } from '@/hooks/use-toast'; @@ -18,16 +25,38 @@ interface TransferModalProps { onClose: () => void; } +type TokenType = 'HEZ' | 'PEZ' | 'USDT' | 'BTC' | 'ETH' | 'DOT'; + +interface Token { + symbol: TokenType; + name: string; + assetId?: number; + decimals: number; + color: string; +} + +const TOKENS: Token[] = [ + { symbol: 'HEZ', name: 'Hez Token', decimals: 12, color: 'from-green-600 to-yellow-400' }, + { symbol: 'PEZ', name: 'Pez Token', assetId: 1, decimals: 12, color: 'from-blue-600 to-purple-400' }, + { symbol: 'USDT', name: 'Tether USD', assetId: 2, decimals: 6, color: 'from-green-500 to-green-600' }, + { symbol: 'BTC', name: 'Bitcoin', assetId: 3, decimals: 8, color: 'from-orange-500 to-yellow-500' }, + { symbol: 'ETH', name: 'Ethereum', assetId: 4, decimals: 18, color: 'from-purple-500 to-blue-500' }, + { symbol: 'DOT', name: 'Polkadot', assetId: 5, decimals: 10, color: 'from-pink-500 to-red-500' }, +]; + export const TransferModal: React.FC = ({ isOpen, onClose }) => { const { api, isApiReady, selectedAccount } = usePolkadot(); const { toast } = useToast(); + const [selectedToken, setSelectedToken] = useState('HEZ'); const [recipient, setRecipient] = useState(''); const [amount, setAmount] = useState(''); const [isTransferring, setIsTransferring] = useState(false); const [txStatus, setTxStatus] = useState<'idle' | 'signing' | 'pending' | 'success' | 'error'>('idle'); const [txHash, setTxHash] = useState(''); + const currentToken = TOKENS.find(t => t.symbol === selectedToken) || TOKENS[0]; + const handleTransfer = async () => { if (!api || !isApiReady || !selectedAccount) { toast({ @@ -55,12 +84,22 @@ export const TransferModal: React.FC = ({ isOpen, onClose }) const { web3FromAddress } = await import('@polkadot/extension-dapp'); const injector = await web3FromAddress(selectedAccount.address); - // Convert amount to plancks (12 decimals) - const decimals = 12; - const amountInPlancks = BigInt(parseFloat(amount) * Math.pow(10, decimals)); + // Convert amount to smallest unit + const amountInSmallestUnit = BigInt(parseFloat(amount) * Math.pow(10, currentToken.decimals)); - // Create transfer transaction - const transfer = api.tx.balances.transferKeepAlive(recipient, amountInPlancks.toString()); + let transfer; + + // Create appropriate transfer transaction based on token type + if (selectedToken === 'HEZ') { + // Native token transfer + transfer = api.tx.balances.transferKeepAlive(recipient, amountInSmallestUnit.toString()); + } else { + // Asset token transfer (PEZ, USDT, BTC, ETH, DOT) + if (!currentToken.assetId) { + throw new Error('Asset ID not configured'); + } + transfer = api.tx.assets.transfer(currentToken.assetId, recipient, amountInSmallestUnit.toString()); + } setTxStatus('pending'); @@ -96,7 +135,7 @@ export const TransferModal: React.FC = ({ isOpen, onClose }) setTxStatus('success'); toast({ title: "Transfer Successful!", - description: `Sent ${amount} HEZ to ${recipient.slice(0, 8)}...${recipient.slice(-6)}`, + description: `Sent ${amount} ${selectedToken} to ${recipient.slice(0, 8)}...${recipient.slice(-6)}`, }); // Reset form after 2 seconds @@ -133,6 +172,7 @@ export const TransferModal: React.FC = ({ isOpen, onClose }) setAmount(''); setTxStatus('idle'); setTxHash(''); + setSelectedToken('HEZ'); onClose(); } }; @@ -141,9 +181,9 @@ export const TransferModal: React.FC = ({ isOpen, onClose }) - Send HEZ + Send Tokens - Transfer HEZ tokens to another account + Transfer tokens to another account @@ -175,6 +215,31 @@ export const TransferModal: React.FC = ({ isOpen, onClose }) ) : (
+ {/* Token Selection */} +
+ + +
+
= ({ isOpen, onClose })
- + setAmount(e.target.value)} placeholder="0.0000" className="bg-gray-800 border-gray-700 text-white mt-2" disabled={isTransferring} /> +
+ Decimals: {currentToken.decimals} +
{txStatus === 'signing' && ( @@ -221,7 +289,7 @@ export const TransferModal: React.FC = ({ isOpen, onClose }) - +
@@ -84,8 +95,18 @@ const WalletDashboard: React.FC = () => { isOpen={isTransferModalOpen} onClose={() => setIsTransferModalOpen(false)} /> + + setIsReceiveModalOpen(false)} + /> + + setIsHistoryModalOpen(false)} + /> ); }; -export default WalletDashboard; +export default WalletDashboard; \ No newline at end of file