diff --git a/package-lock.json b/package-lock.json index 63e3df6..13961f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,12 +12,13 @@ "@cosmjs/stargate": "^0.32.3", "@ethereumjs/common": "^4.4.0", "@ethereumjs/vm": "^8.1.1", - "@kiichain/kiijs-evm": "^0.2.0", + "@kiichain/kiijs-evm": "^0.4.0", "@kiichain/kiijs-proto": "^0.1.3", "@radix-ui/react-avatar": "^1.1.2", "@radix-ui/react-collapsible": "^1.1.2", "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-dropdown-menu": "^2.1.4", + "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.6", @@ -33,7 +34,6 @@ "next": "15.1.3", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-google-recaptcha": "^3.1.0", "react-hot-toast": "^2.5.2", "react-icons": "^5.5.0", "react-toastify": "^11.0.5", @@ -53,7 +53,6 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "@types/react-google-recaptcha": "^2.1.9", "@types/webpack": "^5.28.5", "eslint": "^9", "eslint-config-next": "15.1.3", @@ -3230,9 +3229,9 @@ } }, "node_modules/@kiichain/kiijs-evm": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@kiichain/kiijs-evm/-/kiijs-evm-0.2.0.tgz", - "integrity": "sha512-7ehH64tswOIrlpUv/7YKk9sXcqrXP5e1QX6P9IOiIOHGFYDirr+NyTQZCBy0+zuQe0oxQPw1kyU7gWH28nnZBQ==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@kiichain/kiijs-evm/-/kiijs-evm-0.4.0.tgz", + "integrity": "sha512-FIejNxN7bzJCJ4rAaNmW//wal8qoYtONpUPTUX1nej69jfrBmPZdDXNwgVQ9qNGHLB83krPK6UiF+rZCEfkMUA==", "peerDependencies": { "dotenv": "16.4.7", "ethers": "^6.0.0", @@ -4150,6 +4149,11 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==" + }, "node_modules/@radix-ui/primitive": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", @@ -4630,6 +4634,440 @@ } } }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.5.tgz", + "integrity": "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==" + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", + "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", + "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-popper": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", + "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==" + }, "node_modules/@radix-ui/react-separator": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.1.tgz", @@ -4738,6 +5176,37 @@ } } }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-escape-keydown": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", @@ -4771,6 +5240,20 @@ } } }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-rect": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", @@ -6847,15 +7330,6 @@ "@types/react": "^19.0.0" } }, - "node_modules/@types/react-google-recaptcha": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@types/react-google-recaptcha/-/react-google-recaptcha-2.1.9.tgz", - "integrity": "sha512-nT31LrBDuoSZJN4QuwtQSF3O89FVHC4jLhM+NtKEmVF5R1e8OY0Jo4//x2Yapn2aNHguwgX5doAq8Zo+Ehd0ug==", - "dev": true, - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/readable-stream": { "version": "2.3.15", "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-2.3.15.tgz", @@ -11334,14 +11808,6 @@ "minimalistic-crypto-utils": "^1.0.1" } }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "dependencies": { - "react-is": "^16.7.0" - } - }, "node_modules/idb-keyval": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz", @@ -13588,6 +14054,7 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -13759,18 +14226,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-async-script": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/react-async-script/-/react-async-script-1.2.0.tgz", - "integrity": "sha512-bCpkbm9JiAuMGhkqoAiC0lLkb40DJ0HOEJIku+9JDjxX3Rcs+ztEOG13wbrOskt3n2DTrjshhaQ/iay+SnGg5Q==", - "dependencies": { - "hoist-non-react-statics": "^3.3.0", - "prop-types": "^15.5.0" - }, - "peerDependencies": { - "react": ">=16.4.1" - } - }, "node_modules/react-dom": { "version": "19.0.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", @@ -13783,18 +14238,6 @@ "react": "^19.0.0" } }, - "node_modules/react-google-recaptcha": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-3.1.0.tgz", - "integrity": "sha512-cYW2/DWas8nEKZGD7SCu9BSuVz8iOcOLHChHyi7upUuVhkpkhYG/6N3KDiTQ3XAiZ2UAZkfvYKMfAHOzBOcGEg==", - "dependencies": { - "prop-types": "^15.5.0", - "react-async-script": "^1.2.0" - }, - "peerDependencies": { - "react": ">=16.4.1" - } - }, "node_modules/react-hot-toast": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.2.tgz", @@ -13825,19 +14268,19 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, "license": "MIT" }, "node_modules/react-remove-scroll": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz", - "integrity": "sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==", - "license": "MIT", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", "dependencies": { "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.1", + "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.2" + "use-sidecar": "^1.1.3" }, "engines": { "node": ">=10" diff --git a/package.json b/package.json index 9253d66..621c642 100644 --- a/package.json +++ b/package.json @@ -13,12 +13,13 @@ "@cosmjs/stargate": "^0.32.3", "@ethereumjs/common": "^4.4.0", "@ethereumjs/vm": "^8.1.1", - "@kiichain/kiijs-evm": "^0.2.0", + "@kiichain/kiijs-evm": "^0.4.0", "@kiichain/kiijs-proto": "^0.1.3", "@radix-ui/react-avatar": "^1.1.2", "@radix-ui/react-collapsible": "^1.1.2", "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-dropdown-menu": "^2.1.4", + "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.6", diff --git a/public/images/cosmos-logo.png b/public/images/cosmos-logo.png new file mode 100644 index 0000000..53fead1 Binary files /dev/null and b/public/images/cosmos-logo.png differ diff --git a/src/app/bridge/page.tsx b/src/app/bridge/page.tsx new file mode 100644 index 0000000..ec37938 --- /dev/null +++ b/src/app/bridge/page.tsx @@ -0,0 +1,11 @@ +"use client"; + +import BridgeDashboard from "@/components/Bridge/BridgeDashboard"; + +export default function Page() { + return ( +
+ +
+ ); +} diff --git a/src/components/Bridge/BridgeCard.tsx b/src/components/Bridge/BridgeCard.tsx new file mode 100644 index 0000000..6e47e1c --- /dev/null +++ b/src/components/Bridge/BridgeCard.tsx @@ -0,0 +1,282 @@ +import { useTheme } from "@/context/ThemeContext"; +import { useMemo, useState } from "react"; +import { Card, CardContent } from "../ui/card"; +import { Input } from "../ui/input"; +import { Button } from "../ui/button"; +import { Send } from "lucide-react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "../ui/select"; +import { IBCToken } from "@/services/queries/ibc"; +import { ethers } from "ethers"; +import { formatBalances } from "@/utils/format"; +import { useSendIBC } from "@/services/mutations/ibc"; +import { useAccount, useWalletClient } from "wagmi"; +import { toast } from "sonner"; + +export interface IConnection { + id: string; + name: string; + iconUrl: string; + chainId: string; + status: "active" | "inactive"; + channel: string; + prefix: string; + explorer: string; +} + +interface BridgeCardProps { + connection: IConnection; + balances: IBCToken[] | null; +} + +export default function BridgeCard({ connection, balances }: BridgeCardProps) { + const { theme } = useTheme(); + const [selectedToken, setSelectedToken] = useState(""); + const [address, setAddress] = useState(""); + const [amount, setAmount] = useState(""); + const sendIBCMutation = useSendIBC(); + const { data: walletClient } = useWalletClient(); + + const { address: connectedAddress } = useAccount(); + + const handleSend = async () => { + if (!amount || !address || !selectedTokenInfo || parseFloat(amount) <= 0) + return; + + const regex = new RegExp(`^${connection.prefix}1[0-9a-z]{38}$`); + const isValid = regex.test(address); + + if (!address || !isValid) { + toast.error(`Address must have prefix ${connection.prefix}`); + return; + } + + const amountToSend = ethers.parseUnits(amount, selectedTokenInfo.exponent); + const available = BigInt(selectedTokenInfo.amount); + + if (amountToSend > available) { + toast.error("Insufficient funds to send"); + return; + } + + const mutData = { + amount, + denom: selectedTokenInfo.base, + ibc_channel: connection.channel, + toAddress: address, + walletClient, + exponent: selectedTokenInfo?.exponent, + }; + + await sendIBCMutation.mutate(mutData); + + setAmount(""); + }; + + const selectedTokenInfo = useMemo(() => { + return balances?.find((t) => t.denom === selectedToken); + }, [selectedToken, balances]); + + return ( + + + {/* Header */} +
+
+ {`${connection.name} +
+ +
+

+ {connection.name} +

+

+ {connection.chainId} +

+
+
+
+ + Active + +
+
+ + {/* Token selector */} +
+ + + + {/* Balance */} + {selectedToken && ( +

+ {`Balance: ${formatBalances( + ethers.formatUnits( + selectedTokenInfo?.amount || 0, + selectedTokenInfo?.exponent || 18 + ) + )} ${selectedToken}`} +

+ )} +
+ + {/* Address input */} +
+ + setAddress(e.target.value)} + style={{ + backgroundColor: theme.bgColor, + borderColor: "transparent", + color: theme.primaryTextColor, + }} + className="border-0 placeholder:text-gray-500" + /> +
+ + {/* Amount input */} +
+ + setAmount(e.target.value)} + style={{ + backgroundColor: theme.bgColor, + borderColor: "transparent", + color: theme.primaryTextColor, + MozAppearance: "textfield", + }} + className="border-0 placeholder:text-gray-500 [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" + /> +
+ + {/* Send button */} + + + {/* Show explorer */} +
+

+ {`Want to verify balances on ${connection.name}? `} + + View on Explorer + +

+
+
+
+ ); +} diff --git a/src/components/Bridge/BridgeDashboard.tsx b/src/components/Bridge/BridgeDashboard.tsx new file mode 100644 index 0000000..7c42474 --- /dev/null +++ b/src/components/Bridge/BridgeDashboard.tsx @@ -0,0 +1,139 @@ +import { useTheme } from "@/context/ThemeContext"; +import { Card, CardContent } from "@/components/ui/card"; +import { StatCard } from "../StatCard"; +import BridgeCard from "./BridgeCard"; +import { useAccount, useBalance, useWalletClient } from "wagmi"; +import { + IBCToken, + useCosmosTokens, + useQueryIBCAssetList, +} from "@/services/queries/ibc"; +import type { IConnection } from "./BridgeCard"; +import { useMemo } from "react"; +import { IBC_CONNECTIONS } from "@/config/chain"; + +export default function BridgeDashboard() { + const connections = IBC_CONNECTIONS as IConnection[]; + + const { theme } = useTheme(); + const { data: walletClient } = useWalletClient(); + + const { address } = useAccount(); + const { data: balance } = useBalance({ address }); + const { data: ercTokens } = useCosmosTokens(walletClient); + const { data: ibcAssets } = useQueryIBCAssetList(); + + const combinedBalances = useMemo(() => { + if (!ercTokens || !balance || !ibcAssets) return []; + + const nativeCoin: IBCToken = { + amount: balance.value.toString(), + denom: "kii", + exponent: 18, + base: "akii", + name: "KII", + }; + + const mappedErc = ercTokens + .map((token) => { + const matchingAsset = ibcAssets.find((a) => { + return ( + a.base.startsWith("ibc/") && + a.base.slice(-40).toLowerCase() === + token.contractAddress.toLowerCase().slice(2) + ); + }); + + if (!matchingAsset) return null; + + const balanceInfo: IBCToken = { + amount: token.amount.toString(), + denom: matchingAsset.denom, + exponent: matchingAsset.exponent, + base: matchingAsset.base, + name: matchingAsset.name, + }; + + return balanceInfo; + }) + .filter(Boolean) as IBCToken[]; + + return [...mappedErc, nativeCoin]; + }, [ercTokens, balance, ibcAssets]); + + return ( +
+ {/* Header */} +
+
+

+ IBC Bridges +

+
+

+ Transfer tokens between KiiChain and other Cosmos chains +

+
+ + {/* Stats cards */} +
+ +
+ + {/* Bridge Cards */} +
+ {connections.map((connection) => ( + + ))} +
+ + {/* Info Section */} + + +
+
+ i +
+
+

+ About IBC Transfers +

+

+ Inter-Blockchain Communication (IBC) allows secure token + transfers between different Cosmos chains. Transfers typically + take 1-3 minutes to complete and require a small fee on both + source and destination chains. +

+
+
+
+
+
+ ); +} diff --git a/src/components/ui/icons.tsx b/src/components/ui/icons.tsx index 9e2dd80..fb5a7c4 100644 --- a/src/components/ui/icons.tsx +++ b/src/components/ui/icons.tsx @@ -440,3 +440,26 @@ export function ArrowDownIcon(props: React.SVGProps) { ); } + +export function TransferArrowsIcon(props: React.SVGProps) { + return ( + + + + + + ); +} diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx new file mode 100644 index 0000000..6e637f7 --- /dev/null +++ b/src/components/ui/select.tsx @@ -0,0 +1,159 @@ +"use client" + +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { Check, ChevronDown, ChevronUp } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Select = SelectPrimitive.Root + +const SelectGroup = SelectPrimitive.Group + +const SelectValue = SelectPrimitive.Value + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className + )} + {...props} + > + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +} diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx index 2d8cc97..d9510a8 100644 --- a/src/components/ui/sidebar.tsx +++ b/src/components/ui/sidebar.tsx @@ -18,6 +18,7 @@ import { StakingIcon, GovernanceIcon, SmartContractIaIcon, + TransferArrowsIcon, } from "./icons"; import { useIsMobile } from "@/hooks/use-mobile"; @@ -69,6 +70,7 @@ const Icons = { StakingIcon, GovernanceIcon, SmartContractIaIcon, + TransferArrowsIcon, }; const SidebarProvider = React.forwardRef< @@ -786,16 +788,16 @@ const menuItems = [ label: "Blocks", href: "/blocks", }, + { + icon: , + label: "Bridge", + href: "/bridge", + }, { icon: , label: "Uptime", href: "/uptime", }, - // { - // icon: , - // label: "Supply", - // href: "/supply", - // }, { icon: , label: "Parameters", @@ -811,7 +813,6 @@ const menuItems = [ label: "Faucet", href: "/faucet", }, - { icon: , label: "Deploy Smart Contracts", diff --git a/src/config/chain.ts b/src/config/chain.ts index 61ad16b..02bfd34 100644 --- a/src/config/chain.ts +++ b/src/config/chain.ts @@ -8,8 +8,23 @@ export const CHAIN_RPC_ENDPOINT = "https://rpc.uno.sentry.testnet.v3.kiivalidator.com"; export const CHAIN_JSON_RPC_ENDPOINT = "https://json-rpc.uno.sentry.testnet.v3.kiivalidator.com/"; +export const CHAIN_ASSET_LIST_URL = + "https://raw.githubusercontent.com/KiiChain/testnets/main/testnet_oro/assetlist.json"; export const TESTNET_ORO_EVM = kiiEvm.TESTNET_ORO_EVM; export const KIICHAIN_BASE_DENOM = kiiEvm.KIICHAIN_BASE_DENOM; export const KIICHAIN_SYMBOL = kiiEvm.TESTNET_ORO_EVM.nativeCurrency.symbol; export const KIICHAIN_ORO_DENOM = kiiEvm.ORO_DENOM; + +export const IBC_CONNECTIONS = [ + { + id: "cosmoshub", + name: "Cosmos Hub Testnet", + iconUrl: "/images/cosmos-logo.png", + chainId: "provider", + status: "active", + channel: "channel-3", + prefix: "cosmos", + explorer: "https://explorer.polypore.xyz/provider", + }, +]; diff --git a/src/services/mutations/ibc.ts b/src/services/mutations/ibc.ts new file mode 100644 index 0000000..4a6a5ec --- /dev/null +++ b/src/services/mutations/ibc.ts @@ -0,0 +1,51 @@ +import { SendIBCTokens } from "@/utils/format"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; + +// useSendIBC is the wrapper to the SendIBC Token function +export function useSendIBC() { + const queryClient = useQueryClient(); + + type props = { + toAddress: string; + ibc_channel: string; + amount: string; + denom: string; + exponent: number; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + walletClient: any; + }; + + return useMutation({ + mutationFn: (data: props) => + SendIBCTokens( + data.toAddress, + data.ibc_channel, + data.amount, + "IBC Transfer", + data.walletClient, + data.exponent, + data.denom + ), + + onSuccess: (data) => { + // validate if the transfer was successfully + if (data.status != 1) { + toast.error("Transaction has failed, please try later"); + return; + } + + toast.success("IBC Transfer Successfully Completed"); + + // Invalidate all user-related queries + queryClient.invalidateQueries({ + queryKey: ["user", "cosmosTokens"], + exact: false, + }); + }, + + onError: ({ message }) => { + toast.error(`Error validating task on CosmosHub:, ${message}`); + }, + }); +} diff --git a/src/services/queries/ibc.ts b/src/services/queries/ibc.ts new file mode 100644 index 0000000..e1d7fd0 --- /dev/null +++ b/src/services/queries/ibc.ts @@ -0,0 +1,106 @@ +import { ethers } from "ethers"; +import { useQuery } from "wagmi/query"; +import * as kiiEvm from "@kiichain/kiijs-evm"; +import { CHAIN_ASSET_LIST_URL } from "@/config/chain"; + +interface EvmBalance { + contractAddress: string; + amount: bigint; +} + +export interface IBCToken { + amount: number | string; + denom: string; + exponent: number; + base: string; + name: string; +} + +interface assetList { + chain_name: string; + assets: asset[]; +} + +interface asset { + description: string; + denom_units: { + denom: string; + exponent: number; + }[]; + base: string; + name: string; + display: string; + symbol: string; +} + +// useCosmosTokens retrieves the user tokens on its cosmos-side account using the Bank precompile +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const useCosmosTokens = (walletClient: any) => { + return useQuery({ + queryKey: ["user", "cosmosTokens"], + queryFn: async (): Promise => { + try { + if (!walletClient) throw new Error("Wallet not connected"); + + const ethersProvider = new ethers.BrowserProvider( + walletClient.transport + ); + const signer = await ethersProvider.getSigner(); + const address = await signer.getAddress(); + + const bankPrecompile = kiiEvm.getBankPrecompileEthersV6Contract(signer); + const rawBalances = await bankPrecompile.balances(address); + + const parsed: EvmBalance[] = rawBalances.map( + ([contractAddress, amount]: [string, bigint]) => ({ + contractAddress, + amount, + }) + ); + + return parsed; + } catch (error) { + console.error("Error fetching Cosmos tokens:", error); + return []; + } + }, + enabled: !!walletClient, + staleTime: 300000, + }); +}; + +// useQueryAssetList queries the Kiichain assetlist and returns the IBC assets +export const useQueryIBCAssetList = () => { + return useQuery({ + queryKey: ["assetList"], + queryFn: async (): Promise => { + try { + const res = await fetch(CHAIN_ASSET_LIST_URL); + const json: assetList = await res.json(); + + const ibcAssets = json.assets.filter((a) => a.base.startsWith("ibc/")); + + const mappedBalances: IBCToken[] = ibcAssets.map((asset) => { + const exponent = + asset.denom_units.find((d) => d.denom === asset.display) + ?.exponent ?? 6; + + return { + denom: asset.display, + name: asset.symbol, + exponent, + base: asset.base, + amount: "", + }; + }); + + return mappedBalances; + } catch (error) { + console.error("Error loading asset list:", error); + return []; + } + }, + enabled: true, + staleTime: 5 * 60 * 1000, + }); +}; diff --git a/src/utils/format.ts b/src/utils/format.ts index 5f7a935..73e913e 100644 --- a/src/utils/format.ts +++ b/src/utils/format.ts @@ -1,3 +1,6 @@ +import * as kiiEvm from "@kiichain/kiijs-evm"; +import { ethers } from "ethers"; + export const formatAmount = (amount: string): string => { const num = parseFloat(amount) / 1_000_000_000_000_000_000; return num.toLocaleString(undefined, { @@ -5,3 +8,53 @@ export const formatAmount = (amount: string): string => { maximumFractionDigits: 2, }); }; + +// SendIBCTokens sends native tokens via IBC to the provided channel +export async function SendIBCTokens( + toAddress: string, + channel: string, + amount: string, + memo: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + walletClient: any, + exponent: number, + denom: string +) { + const PORT = "transfer"; + const convertedAmount = ethers.parseUnits(amount, exponent); + + try { + if (!walletClient) throw new Error("Wallet not connected"); + + const ethersProvider = new ethers.BrowserProvider(walletClient.transport); + const signer = await ethersProvider.getSigner(); + + const ibcPrecompile = kiiEvm.getIBCPrecompileEthersV6Contract(signer); + + const tx = await ibcPrecompile.transferWithDefaultTimeout( + toAddress, + PORT, + channel, + denom, + convertedAmount, + memo + ); + + return await tx.wait(); + } catch (error) { + console.error("Error trying to make an IBC transfer:", error); + throw error; + } +} + +// formatBalances formats the input quantity as 123,456.78 +export const formatBalances = (amount: string | number): string => { + const parsed = typeof amount === "string" ? parseFloat(amount) : amount; + + if (isNaN(parsed)) return "0"; + + return parsed.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }); +};