diff --git a/apps/gallery/stories/composites/wui-button.stories.ts b/apps/gallery/stories/composites/wui-button.stories.ts index be15eaf5e7..00512a4282 100644 --- a/apps/gallery/stories/composites/wui-button.stories.ts +++ b/apps/gallery/stories/composites/wui-button.stories.ts @@ -13,19 +13,23 @@ export default { size: 'md', variant: 'fill', disabled: false, + fullWidth: false, iconLeft: undefined, iconRight: undefined, loading: false }, argTypes: { size: { - options: ['sm', 'md', 'lg'], + options: ['xs', 'sm', 'md', 'lg'], control: { type: 'select' } }, variant: { options: buttonOptions, control: { type: 'select' } }, + fullWidth: { + control: { type: 'boolean' } + }, disabled: { control: { type: 'boolean' } }, @@ -49,6 +53,7 @@ export const Default: Component = { size=${args.size} ?loading=${args.loading} ?disabled=${args.disabled} + ?fullWidth=${args.fullWidth} variant=${args.variant} > ${args.iconLeft diff --git a/apps/gallery/stories/composites/wui-details-group.stories.ts b/apps/gallery/stories/composites/wui-details-group.stories.ts new file mode 100644 index 0000000000..6b917dbff3 --- /dev/null +++ b/apps/gallery/stories/composites/wui-details-group.stories.ts @@ -0,0 +1,27 @@ +import type { Meta } from '@storybook/web-components' +import '@web3modal/ui/src/composites/wui-cta-button' +import type { WuiDetailsGroup } from '@web3modal/ui/src/composites/wui-details-group' +import { html } from 'lit' +import '../../components/gallery-container' + +type Component = Meta + +export default { + title: 'Composites/wui-details-group', + args: {} +} as Component + +export const Default: Component = { + render: () => html` + + + + 2 AVAX + + + 0x276...f0ed7 + + + + ` +} diff --git a/apps/gallery/stories/composites/wui-token-list-item.stories.ts b/apps/gallery/stories/composites/wui-token-list-item.stories.ts new file mode 100644 index 0000000000..9a7d8698c8 --- /dev/null +++ b/apps/gallery/stories/composites/wui-token-list-item.stories.ts @@ -0,0 +1,34 @@ +import type { Meta } from '@storybook/web-components' +import '@web3modal/ui/src/composites/wui-list-item' +import type { WuiTokenListItem } from '@web3modal/ui/src/composites/wui-token-list-item' +import { html } from 'lit' +import '../../components/gallery-container' +import { networkImageSrc } from '../../utils/PresetUtils' + +type Component = Meta + +export default { + title: 'Composites/wui-token-list-item', + args: { + name: 'Ethereum', + symbol: 'ETH', + price: '$1,740.72', + amount: '0.867', + imageSrc: networkImageSrc + }, + argTypes: {} +} as Component + +export const Default: Component = { + render: args => + html` + + + ` +} diff --git a/package-lock.json b/package-lock.json index 3aae001939..01c27898ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1573,25 +1573,25 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.1.tgz", - "integrity": "sha512-Pc65opHDliVpRHuKfzI+gSA4zcgr65O4cl64fFJIWEEh8JoHIHh0Oez1Eo8Arz8zq/JhgKodQaxEwUPRtZylVA==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", + "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.24.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.3.tgz", - "integrity": "sha512-5FcvN1JHw2sHJChotgx8Ek0lyuh4kCKelgMTTqhYJJtloNvUfpAFMeNQUtdlIaktwrSV9LtCdqwk48wL2wBacQ==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.4.tgz", + "integrity": "sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.1", + "@babel/generator": "^7.24.4", "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.1", - "@babel/parser": "^7.24.1", + "@babel/helpers": "^7.24.4", + "@babel/parser": "^7.24.4", "@babel/template": "^7.24.0", "@babel/traverse": "^7.24.1", "@babel/types": "^7.24.0", @@ -1623,9 +1623,9 @@ } }, "node_modules/@babel/generator": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.1.tgz", - "integrity": "sha512-DfCRfZsBcrPEHUfuBMgbJ1Ut01Y/itOs+hY2nFLgqsqXd52/iSiVq5TITtUasIUgm+IIKdY2/1I7auiQOEeC9A==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.4.tgz", + "integrity": "sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==", "dependencies": { "@babel/types": "^7.24.0", "@jridgewell/gen-mapping": "^0.3.5", @@ -1682,9 +1682,9 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.1.tgz", - "integrity": "sha512-1yJa9dX9g//V6fDebXoEfEsxkZHk3Hcbm+zLhyu6qVgYFLvmTALTeV+jNU9e5RnYtioBrGEOdoI2joMSNQ/+aA==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.4.tgz", + "integrity": "sha512-lG75yeuUSVu0pIcbhiYMXBXANHrpUPaOfu7ryAzskCgKUHuAxRQI5ssrtmF0X9UXldPlvT0XM/A4F44OXRt6iQ==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.20", @@ -1943,9 +1943,9 @@ } }, "node_modules/@babel/helpers": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.1.tgz", - "integrity": "sha512-BpU09QqEe6ZCHuIHFphEFgvNSrubve1FtyMton26ekZ85gRGi6LrTF7zArARp2YvyFxloeiRmtSCq5sjh1WqIg==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.4.tgz", + "integrity": "sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw==", "dependencies": { "@babel/template": "^7.24.0", "@babel/traverse": "^7.24.1", @@ -2034,9 +2034,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.1.tgz", - "integrity": "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz", + "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==", "bin": { "parser": "bin/babel-parser.js" }, @@ -2044,6 +2044,21 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.4.tgz", + "integrity": "sha512-qpl6vOOEEzTLLcsuqYYo8yDtrTocmu2xkGvgNebvPjT9DTtfFYGmgDqY+rBYXNlqL4s9qLDn6xkrJv4RxAPiTA==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.24.1", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.1.tgz", @@ -2577,9 +2592,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.1.tgz", - "integrity": "sha512-h71T2QQvDgM2SmT29UYU6ozjMlAt7s7CSs5Hvy8f8cf/GM/Z4a2zMfN+fjVGaieeCrXR3EdQl6C4gQG+OgmbKw==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.4.tgz", + "integrity": "sha512-nIFUZIpGKDf9O9ttyRXpHFpKC+X3Y5mtshZONuEUYBomAKoM4y029Jr+uB1bHGPhNmK8YXHevDtKDOLmtRrp6g==", "dependencies": { "@babel/helper-plugin-utils": "^7.24.0" }, @@ -2606,11 +2621,11 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.1.tgz", - "integrity": "sha512-FUHlKCn6J3ERiu8Dv+4eoz7w8+kFLSyeVG4vDAikwADGjUCoHw/JHokyGtr8OR4UjpwPVivyF+h8Q5iv/JmrtA==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.4.tgz", + "integrity": "sha512-B8q7Pz870Hz/q9UgP8InNpY01CSLDSCyqX7zcRuv3FcPl87A2G17lASroHWaCtbdIcbYzOZ7kWmXFKbijMSmFg==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.1", + "@babel/helper-create-class-features-plugin": "^7.24.4", "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-class-static-block": "^7.14.5" }, @@ -3294,12 +3309,12 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.1.tgz", - "integrity": "sha512-liYSESjX2fZ7JyBFkYG78nfvHlMKE6IpNdTVnxmlYUR+j5ZLsitFbaAE+eJSK2zPPkNWNw4mXL51rQ8WrvdK0w==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.4.tgz", + "integrity": "sha512-79t3CQ8+oBGk/80SQ8MN3Bs3obf83zJ0YZjDmDaEZN8MqhMI760apl5z6a20kFeMXBwJX99VpKT8CKxEBp5H1g==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.24.1", + "@babel/helper-create-class-features-plugin": "^7.24.4", "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-typescript": "^7.24.1" }, @@ -3370,14 +3385,15 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.24.3", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.3.tgz", - "integrity": "sha512-fSk430k5c2ff8536JcPvPWK4tZDwehWLGlBp0wrsBUjZVdeQV6lePbwKWZaZfK2vnh/1kQX1PzAJWsnBmVgGJA==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.4.tgz", + "integrity": "sha512-7Kl6cSmYkak0FK/FXjSEnLJ1N9T/WA2RkMhu17gZ/dsxKJUuTYNIylahPTzqpLyJN4WhDif8X0XK1R8Wsguo/A==", "dependencies": { - "@babel/compat-data": "^7.24.1", + "@babel/compat-data": "^7.24.4", "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.4", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.1", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.24.1", @@ -3404,9 +3420,9 @@ "@babel/plugin-transform-async-generator-functions": "^7.24.3", "@babel/plugin-transform-async-to-generator": "^7.24.1", "@babel/plugin-transform-block-scoped-functions": "^7.24.1", - "@babel/plugin-transform-block-scoping": "^7.24.1", + "@babel/plugin-transform-block-scoping": "^7.24.4", "@babel/plugin-transform-class-properties": "^7.24.1", - "@babel/plugin-transform-class-static-block": "^7.24.1", + "@babel/plugin-transform-class-static-block": "^7.24.4", "@babel/plugin-transform-classes": "^7.24.1", "@babel/plugin-transform-computed-properties": "^7.24.1", "@babel/plugin-transform-destructuring": "^7.24.1", @@ -3635,9 +3651,9 @@ "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" }, "node_modules/@babel/runtime": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", - "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz", + "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -5583,9 +5599,9 @@ } }, "node_modules/@emotion/serialize": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.3.tgz", - "integrity": "sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.4.tgz", + "integrity": "sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==", "dependencies": { "@emotion/hash": "^0.9.1", "@emotion/memoize": "^0.8.1", @@ -6978,9 +6994,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "dev": true }, "node_modules/@isaacs/cliui": { @@ -7520,18 +7536,6 @@ "node": ">=14.0.0" } }, - "node_modules/@metamask/json-rpc-engine/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/@metamask/object-multiplex": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@metamask/object-multiplex/-/object-multiplex-1.3.0.tgz", @@ -7658,18 +7662,6 @@ "node": ">=14.0.0" } }, - "node_modules/@metamask/rpc-errors/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/@metamask/safe-event-emitter": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@metamask/safe-event-emitter/-/safe-event-emitter-2.0.0.tgz", @@ -7742,6 +7734,14 @@ "node-fetch": "^2.6.12" } }, + "node_modules/@metamask/sdk-communication-layer/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@metamask/sdk-install-modal-web": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/@metamask/sdk-install-modal-web/-/sdk-install-modal-web-0.14.1.tgz", @@ -7800,6 +7800,14 @@ "@babel/runtime": "^7.20.6" } }, + "node_modules/@metamask/sdk/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@metamask/utils": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-5.0.2.tgz", @@ -10254,9 +10262,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.2.tgz", - "integrity": "sha512-3XFIDKWMFZrMnao1mJhnOT1h2g0169Os848NhhmGweEcfJ4rCi+3yMCOLG4zA61rbJdkcrM/DjVZm9Hg5p5w7g==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.0.tgz", + "integrity": "sha512-jwXtxYbRt1V+CdQSy6Z+uZti7JF5irRKF8hlKfEnF/xJpcNGuuiZMBvuoYM+x9sr9iWGnzrlM0+9hvQ1kgkf1w==", "cpu": [ "arm" ], @@ -10267,9 +10275,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.2.tgz", - "integrity": "sha512-GdxxXbAuM7Y/YQM9/TwwP+L0omeE/lJAR1J+olu36c3LqqZEBdsIWeQ91KBe6nxwOnb06Xh7JS2U5ooWU5/LgQ==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.0.tgz", + "integrity": "sha512-fI9nduZhCccjzlsA/OuAwtFGWocxA4gqXGTLvOyiF8d+8o0fZUeSztixkYjcGq1fGZY3Tkq4yRvHPFxU+jdZ9Q==", "cpu": [ "arm64" ], @@ -10280,9 +10288,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.2.tgz", - "integrity": "sha512-mCMlpzlBgOTdaFs83I4XRr8wNPveJiJX1RLfv4hggyIVhfB5mJfN4P8Z6yKh+oE4Luz+qq1P3kVdWrCKcMYrrA==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.0.tgz", + "integrity": "sha512-BcnSPRM76/cD2gQC+rQNGBN6GStBs2pl/FpweW8JYuz5J/IEa0Fr4AtrPv766DB/6b2MZ/AfSIOSGw3nEIP8SA==", "cpu": [ "arm64" ], @@ -10293,9 +10301,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.2.tgz", - "integrity": "sha512-yUoEvnH0FBef/NbB1u6d3HNGyruAKnN74LrPAfDQL3O32e3k3OSfLrPgSJmgb3PJrBZWfPyt6m4ZhAFa2nZp2A==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.0.tgz", + "integrity": "sha512-LDyFB9GRolGN7XI6955aFeI3wCdCUszFWumWU0deHA8VpR3nWRrjG6GtGjBrQxQKFevnUTHKCfPR4IvrW3kCgQ==", "cpu": [ "x64" ], @@ -10306,9 +10314,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.2.tgz", - "integrity": "sha512-GYbLs5ErswU/Xs7aGXqzc3RrdEjKdmoCrgzhJWyFL0r5fL3qd1NPcDKDowDnmcoSiGJeU68/Vy+OMUluRxPiLQ==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.0.tgz", + "integrity": "sha512-ygrGVhQP47mRh0AAD0zl6QqCbNsf0eTo+vgwkY6LunBcg0f2Jv365GXlDUECIyoXp1kKwL5WW6rsO429DBY/bA==", "cpu": [ "arm" ], @@ -10319,9 +10327,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.2.tgz", - "integrity": "sha512-L1+D8/wqGnKQIlh4Zre9i4R4b4noxzH5DDciyahX4oOz62CphY7WDWqJoQ66zNR4oScLNOqQJfNSIAe/6TPUmQ==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.0.tgz", + "integrity": "sha512-x+uJ6MAYRlHGe9wi4HQjxpaKHPM3d3JjqqCkeC5gpnnI6OWovLdXTpfa8trjxPLnWKyBsSi5kne+146GAxFt4A==", "cpu": [ "arm64" ], @@ -10332,9 +10340,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.2.tgz", - "integrity": "sha512-tK5eoKFkXdz6vjfkSTCupUzCo40xueTOiOO6PeEIadlNBkadH1wNOH8ILCPIl8by/Gmb5AGAeQOFeLev7iZDOA==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.0.tgz", + "integrity": "sha512-nrRw8ZTQKg6+Lttwqo6a2VxR9tOroa2m91XbdQ2sUUzHoedXlsyvY1fN4xWdqz8PKmf4orDwejxXHjh7YBGUCA==", "cpu": [ "arm64" ], @@ -10345,9 +10353,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.13.2.tgz", - "integrity": "sha512-zvXvAUGGEYi6tYhcDmb9wlOckVbuD+7z3mzInCSTACJ4DQrdSLPNUeDIcAQW39M3q6PDquqLWu7pnO39uSMRzQ==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.0.tgz", + "integrity": "sha512-xV0d5jDb4aFu84XKr+lcUJ9y3qpIWhttO3Qev97z8DKLXR62LC3cXT/bMZXrjLF9X+P5oSmJTzAhqwUbY96PnA==", "cpu": [ "ppc64le" ], @@ -10358,9 +10366,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.2.tgz", - "integrity": "sha512-C3GSKvMtdudHCN5HdmAMSRYR2kkhgdOfye4w0xzyii7lebVr4riCgmM6lRiSCnJn2w1Xz7ZZzHKuLrjx5620kw==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.0.tgz", + "integrity": "sha512-SDDhBQwZX6LPRoPYjAZWyL27LbcBo7WdBFWJi5PI9RPCzU8ijzkQn7tt8NXiXRiFMJCVpkuMkBf4OxSxVMizAw==", "cpu": [ "riscv64" ], @@ -10371,9 +10379,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.13.2.tgz", - "integrity": "sha512-l4U0KDFwzD36j7HdfJ5/TveEQ1fUTjFFQP5qIt9gBqBgu1G8/kCaq5Ok05kd5TG9F8Lltf3MoYsUMw3rNlJ0Yg==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.0.tgz", + "integrity": "sha512-RxB/qez8zIDshNJDufYlTT0ZTVut5eCpAZ3bdXDU9yTxBzui3KhbGjROK2OYTTor7alM7XBhssgoO3CZ0XD3qA==", "cpu": [ "s390x" ], @@ -10384,9 +10392,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.2.tgz", - "integrity": "sha512-xXMLUAMzrtsvh3cZ448vbXqlUa7ZL8z0MwHp63K2IIID2+DeP5iWIT6g1SN7hg1VxPzqx0xZdiDM9l4n9LRU1A==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.0.tgz", + "integrity": "sha512-C6y6z2eCNCfhZxT9u+jAM2Fup89ZjiG5pIzZIDycs1IwESviLxwkQcFRGLjnDrP+PT+v5i4YFvlcfAs+LnreXg==", "cpu": [ "x64" ], @@ -10397,9 +10405,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.2.tgz", - "integrity": "sha512-M/JYAWickafUijWPai4ehrjzVPKRCyDb1SLuO+ZyPfoXgeCEAlgPkNXewFZx0zcnoIe3ay4UjXIMdXQXOZXWqA==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.0.tgz", + "integrity": "sha512-i0QwbHYfnOMYsBEyjxcwGu5SMIi9sImDVjDg087hpzXqhBSosxkE7gyIYFHgfFl4mr7RrXksIBZ4DoLoP4FhJg==", "cpu": [ "x64" ], @@ -10410,9 +10418,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.2.tgz", - "integrity": "sha512-2YWwoVg9KRkIKaXSh0mz3NmfurpmYoBBTAXA9qt7VXk0Xy12PoOP40EFuau+ajgALbbhi4uTj3tSG3tVseCjuA==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.0.tgz", + "integrity": "sha512-Fq52EYb0riNHLBTAcL0cun+rRwyZ10S9vKzhGKKgeD+XbwunszSY0rVMco5KbOsTlwovP2rTOkiII/fQ4ih/zQ==", "cpu": [ "arm64" ], @@ -10423,9 +10431,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.2.tgz", - "integrity": "sha512-2FSsE9aQ6OWD20E498NYKEQLneShWes0NGMPQwxWOdws35qQXH+FplabOSP5zEe1pVjurSDOGEVCE2agFwSEsw==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.0.tgz", + "integrity": "sha512-e/PBHxPdJ00O9p5Ui43+vixSgVf4NlLsmV6QneGERJ3lnjIua/kim6PRFe3iDueT1rQcgSkYP8ZBBXa/h4iPvw==", "cpu": [ "ia32" ], @@ -10436,9 +10444,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.2.tgz", - "integrity": "sha512-7h7J2nokcdPePdKykd8wtc8QqqkqxIrUz7MHj6aNr8waBRU//NLDVnNjQnqQO6fqtjrtCdftpbTuOKAyrAQETQ==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.0.tgz", + "integrity": "sha512-aGg7iToJjdklmxlUlJh/PaPNa4PmqHfyRMLunbL3eaMO0gp656+q1zOKkpJ/CVe9CryJv6tAN1HDoR8cNGzkag==", "cpu": [ "x64" ], @@ -10755,13 +10763,13 @@ } }, "node_modules/@smithy/core": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.4.0.tgz", - "integrity": "sha512-uu9ZDI95Uij4qk+L6kyFjdk11zqBkcJ3Lv0sc6jZrqHvLyr0+oeekD3CnqMafBn/5PRI6uv6ulW3kNLRBUHeVw==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.4.1.tgz", + "integrity": "sha512-jCnbEQHvTOUQXxXOS110FIMc83dCXUlrqiG/q0QzUSYhglDj9bJVPFjXmxc6qUfARe0mEb8h9LeVoh7FUYHuUg==", "dev": true, "dependencies": { "@smithy/middleware-endpoint": "^2.5.0", - "@smithy/middleware-retry": "^2.2.0", + "@smithy/middleware-retry": "^2.3.0", "@smithy/middleware-serde": "^2.3.0", "@smithy/protocol-http": "^3.3.0", "@smithy/smithy-client": "^2.5.0", @@ -10884,9 +10892,9 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-2.2.0.tgz", - "integrity": "sha512-PsjDOLpbevgn37yJbawmfVoanru40qVA8UEf2+YA1lvOefmhuhL6ZbKtGsLAWDRnE1OlAmedsbA/htH6iSZjNA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-2.3.0.tgz", + "integrity": "sha512-5H7kD0My2RkZryvYIWA4C9w6t/pdJfbgEdq+fcZhbnZsqHm/4vYFVjDsOzb5pC7PEpksuijoM9fGbM6eN4rLSg==", "dev": true, "dependencies": { "@smithy/node-config-provider": "^2.3.0", @@ -10897,7 +10905,7 @@ "@smithy/util-middleware": "^2.2.0", "@smithy/util-retry": "^2.2.0", "tslib": "^2.6.2", - "uuid": "^8.3.2" + "uuid": "^9.0.1" }, "engines": { "node": ">=14.0.0" @@ -11492,18 +11500,6 @@ "@solana/web3.js": "*" } }, - "node_modules/@solflare-wallet/metamask-sdk/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/@solflare-wallet/sdk": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/@solflare-wallet/sdk/-/sdk-1.4.2.tgz", @@ -11517,18 +11513,6 @@ "@solana/web3.js": "*" } }, - "node_modules/@solflare-wallet/sdk/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/@spruceid/siwe-parser": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@spruceid/siwe-parser/-/siwe-parser-2.0.2.tgz", @@ -11704,19 +11688,6 @@ "url": "https://opencollective.com/storybook" } }, - "node_modules/@storybook/addon-actions/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/@storybook/addon-backgrounds": { "version": "7.6.7", "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-7.6.7.tgz", @@ -12461,9 +12432,9 @@ } }, "node_modules/@storybook/core-common/node_modules/@types/node": { - "version": "18.19.26", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.26.tgz", - "integrity": "sha512-+wiMJsIwLOYCvUqSdKTrfkS8mpTp+MPINe6+Np4TAGFWWRWiBQ5kSq9nZGCSPkzx9mvT+uEukzpX4MOSCydcvw==", + "version": "18.19.29", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.29.tgz", + "integrity": "sha512-5pAX7ggTmWZdhUrhRWLPf+5oM7F80bcKVCBbr0zwEkTNzTJL2CWQjznpFgHYy6GrzkYi2Yjy7DHKoynFxqPV8g==", "dependencies": { "undici-types": "~5.26.4" } @@ -12587,9 +12558,9 @@ } }, "node_modules/@storybook/core-server/node_modules/@types/node": { - "version": "18.19.26", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.26.tgz", - "integrity": "sha512-+wiMJsIwLOYCvUqSdKTrfkS8mpTp+MPINe6+Np4TAGFWWRWiBQ5kSq9nZGCSPkzx9mvT+uEukzpX4MOSCydcvw==", + "version": "18.19.29", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.29.tgz", + "integrity": "sha512-5pAX7ggTmWZdhUrhRWLPf+5oM7F80bcKVCBbr0zwEkTNzTJL2CWQjznpFgHYy6GrzkYi2Yjy7DHKoynFxqPV8g==", "dependencies": { "undici-types": "~5.26.4" } @@ -13493,13 +13464,13 @@ } }, "node_modules/@types/serve-static": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", - "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", "dependencies": { "@types/http-errors": "*", - "@types/mime": "*", - "@types/node": "*" + "@types/node": "*", + "@types/send": "*" } }, "node_modules/@types/stack-utils": { @@ -14218,9 +14189,9 @@ "integrity": "sha512-tr7XntDAu50BVENgQfajMLzacmSe34D+qZc4zjnniz0ZVuw/TZcLcyxHQjYpJTM36sGEkZZlYLnIM1hH7alTMA==" }, "node_modules/@walletconnect/core": { - "version": "2.11.3", - "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.11.3.tgz", - "integrity": "sha512-/9m4EqiggFUwkQDv5PDWbcTI+yCVnBd/iYW5iIHEkivg2/mnBr2bQz2r/vtPjp19r/ZK62Dx0+UN3U+BWP8ulQ==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.12.0.tgz", + "integrity": "sha512-CORck4dRvCpIn6hl2ZtUnjrSJ0JHt9TRteGCViwPyXNSuvXz70RvaIkvPoybYZBGCRQR4WTJ4dMdqeQpuyrL/g==", "dependencies": { "@walletconnect/heartbeat": "1.2.1", "@walletconnect/jsonrpc-provider": "1.0.13", @@ -14228,13 +14199,13 @@ "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/jsonrpc-ws-connection": "1.0.14", "@walletconnect/keyvaluestorage": "^1.1.1", - "@walletconnect/logger": "^2.0.1", + "@walletconnect/logger": "^2.1.0", "@walletconnect/relay-api": "^1.0.9", "@walletconnect/relay-auth": "^1.0.4", "@walletconnect/safe-json": "^1.0.2", "@walletconnect/time": "^1.0.2", - "@walletconnect/types": "2.11.3", - "@walletconnect/utils": "2.11.3", + "@walletconnect/types": "2.12.0", + "@walletconnect/utils": "2.12.0", "events": "^3.3.0", "isomorphic-unfetch": "3.1.0", "lodash.isequal": "4.5.0", @@ -14242,9 +14213,9 @@ } }, "node_modules/@walletconnect/core/node_modules/@walletconnect/types": { - "version": "2.11.3", - "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.11.3.tgz", - "integrity": "sha512-JY4wA9MVosDW9dcJMTpnwliste0aJGJ1X6Q4ulLsQsgWRSEBRkLila0oUT01TDBW9Yq8uUp7uFOUTaKx6KWVAg==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.12.0.tgz", + "integrity": "sha512-uhB3waGmujQVJcPgJvGOpB8RalgYSBT+HpmVbfl4Qe0xJyqpRUo4bPjQa0UYkrHaW20xIw94OuP4+FMLYdeemg==", "dependencies": { "@walletconnect/events": "^1.0.1", "@walletconnect/heartbeat": "1.2.1", @@ -14544,19 +14515,14 @@ } }, "node_modules/@walletconnect/logger": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@walletconnect/logger/-/logger-2.0.1.tgz", - "integrity": "sha512-SsTKdsgWm+oDTBeNE/zHxxr5eJfZmE9/5yp/Ku+zJtcTAjELb3DXueWkDXmE9h8uHIbJzIb5wj5lPdzyrjT6hQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@walletconnect/logger/-/logger-2.1.0.tgz", + "integrity": "sha512-lyCRHlxlBHxvj1fJXa2YOW4whVNucPKF7Oc0D1UvYhfArpIIjlJJiTe5cLm8g4ZH4z5lKp14N/c9oRHlyv5v4A==", "dependencies": { - "pino": "7.11.0", - "tslib": "1.14.1" + "@walletconnect/safe-json": "^1.0.2", + "pino": "7.11.0" } }, - "node_modules/@walletconnect/logger/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, "node_modules/@walletconnect/mobile-registry": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@walletconnect/mobile-registry/-/mobile-registry-1.4.0.tgz", @@ -14874,25 +14840,25 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "node_modules/@walletconnect/sign-client": { - "version": "2.11.3", - "resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.11.3.tgz", - "integrity": "sha512-JVjLTxN/3NjMXv5zalSGKuSYLRyU2yX6AWEdq17cInlrwODpbWZr6PS1uxMWdH4r90DXBLhdtwDbEq/pfd0BPg==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.12.0.tgz", + "integrity": "sha512-JUHJVZtW9iJmn3I2byLzhMRSFiQicTPU92PLuHIF2nG98CqsvlPn8Cu8Cx5CEPFrxPQWwLA+Dv/F+wuSgQiD/w==", "dependencies": { - "@walletconnect/core": "2.11.3", + "@walletconnect/core": "2.12.0", "@walletconnect/events": "^1.0.1", "@walletconnect/heartbeat": "1.2.1", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/logger": "^2.0.1", "@walletconnect/time": "^1.0.2", - "@walletconnect/types": "2.11.3", - "@walletconnect/utils": "2.11.3", + "@walletconnect/types": "2.12.0", + "@walletconnect/utils": "2.12.0", "events": "^3.3.0" } }, "node_modules/@walletconnect/sign-client/node_modules/@walletconnect/types": { - "version": "2.11.3", - "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.11.3.tgz", - "integrity": "sha512-JY4wA9MVosDW9dcJMTpnwliste0aJGJ1X6Q4ulLsQsgWRSEBRkLila0oUT01TDBW9Yq8uUp7uFOUTaKx6KWVAg==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.12.0.tgz", + "integrity": "sha512-uhB3waGmujQVJcPgJvGOpB8RalgYSBT+HpmVbfl4Qe0xJyqpRUo4bPjQa0UYkrHaW20xIw94OuP4+FMLYdeemg==", "dependencies": { "@walletconnect/events": "^1.0.1", "@walletconnect/heartbeat": "1.2.1", @@ -15041,9 +15007,9 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "node_modules/@walletconnect/utils": { - "version": "2.11.3", - "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.11.3.tgz", - "integrity": "sha512-jsdNkrl/IcTkzWFn0S2d0urzBXg6RxVJtUYRsUx3qI3wzOGiABP9ui3yiZ3SgZOv9aRe62PaNp1qpbYZ+zPb8Q==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.12.0.tgz", + "integrity": "sha512-GIpfHUe1Bjp1Tjda0SkJEizKOT2biuv7VPFnKsOLT1T+8QxEP9NruC+K2UUEvijS1Qr/LKH9P5004RYNgrch+w==", "dependencies": { "@stablelib/chacha20poly1305": "1.0.1", "@stablelib/hkdf": "1.0.1", @@ -15053,7 +15019,7 @@ "@walletconnect/relay-api": "^1.0.9", "@walletconnect/safe-json": "^1.0.2", "@walletconnect/time": "^1.0.2", - "@walletconnect/types": "2.11.3", + "@walletconnect/types": "2.12.0", "@walletconnect/window-getters": "^1.0.1", "@walletconnect/window-metadata": "^1.0.1", "detect-browser": "5.3.0", @@ -15062,9 +15028,9 @@ } }, "node_modules/@walletconnect/utils/node_modules/@walletconnect/types": { - "version": "2.11.3", - "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.11.3.tgz", - "integrity": "sha512-JY4wA9MVosDW9dcJMTpnwliste0aJGJ1X6Q4ulLsQsgWRSEBRkLila0oUT01TDBW9Yq8uUp7uFOUTaKx6KWVAg==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.12.0.tgz", + "integrity": "sha512-uhB3waGmujQVJcPgJvGOpB8RalgYSBT+HpmVbfl4Qe0xJyqpRUo4bPjQa0UYkrHaW20xIw94OuP4+FMLYdeemg==", "dependencies": { "@walletconnect/events": "^1.0.1", "@walletconnect/heartbeat": "1.2.1", @@ -16032,6 +15998,14 @@ "node": ">= 10.0.0" } }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -16542,9 +16516,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001600", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001600.tgz", - "integrity": "sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==", + "version": "1.0.30001605", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001605.tgz", + "integrity": "sha512-nXwGlFWo34uliI9z3n6Qc0wZaf7zaZWA1CPZ169La5mV3I/gem7bst0vr5XQH5TJXZIMfDeZyOrZnSlVzKxxHQ==", "funding": [ { "type": "opencollective", @@ -17254,9 +17228,9 @@ } }, "node_modules/cookie-es": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.0.0.tgz", - "integrity": "sha512-mWYvfOLrfEc996hlKcdABeIiPHUPC6DM2QYZdGGOvhOTbA3tjm2eBwqlJpoFdjC89NI4Qt6h0Pu06Mp+1Pj5OQ==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.1.0.tgz", + "integrity": "sha512-L2rLOcK0wzWSfSDA33YR+PUHDG10a8px7rUHKWbGLP4YfbsMed2KFUw5fczvDPbT98DDe3LEzviswl810apTEw==" }, "node_modules/cookie-signature": { "version": "1.0.6", @@ -18136,9 +18110,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.721", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.721.tgz", - "integrity": "sha512-k1x2r6foI8iJOp+1qTxbbrrWMsOiHkzGBYwYigaq+apO1FSqtn44KTo3Sy69qt7CRr7149zTcsDvH7MUKsOuIQ==" + "version": "1.4.724", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.724.tgz", + "integrity": "sha512-RTRvkmRkGhNBPPpdrgtDKvmOEYTrPlXDfc0J/Nfq5s29tEahAwhiX4mmhNzj6febWMleulxVYPh7QwCSL/EldA==" }, "node_modules/elliptic": { "version": "6.5.4", @@ -18319,9 +18293,9 @@ } }, "node_modules/es-abstract": { - "version": "1.23.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.2.tgz", - "integrity": "sha512-60s3Xv2T2p1ICykc7c+DNDPLDMm9t4QxCOUU0K9JxiLjM3C1zB9YVdN7tjxrFd4+AkZ8CdX1ovUga4P2+1e+/w==", + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.1", @@ -18363,11 +18337,11 @@ "safe-regex-test": "^1.0.3", "string.prototype.trim": "^1.2.9", "string.prototype.trimend": "^1.0.8", - "string.prototype.trimstart": "^1.0.7", + "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.2", "typed-array-byte-length": "^1.0.1", "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.5", + "typed-array-length": "^1.0.6", "unbox-primitive": "^1.0.2", "which-typed-array": "^1.1.15" }, @@ -20959,9 +20933,9 @@ } }, "node_modules/i18next-browser-languagedetector": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.0.tgz", - "integrity": "sha512-U00DbDtFIYD3wkWsr2aVGfXGAj2TgnELzOX9qv8bT0aJtvPV9CRO77h+vgmHFBMe7LAxdwvT/7VkCWGya6L3tA==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.1.tgz", + "integrity": "sha512-h/pM34bcH6tbz8WgGXcmWauNpQupCGr25XPp9cZwZInR9XHSjIFDYp1SIok7zSPsTOMxdvuLyu86V+g2Kycnfw==", "dependencies": { "@babel/runtime": "^7.23.2" } @@ -21959,6 +21933,14 @@ "node": ">=6.14.2" } }, + "node_modules/jayson/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/jayson/node_modules/ws": { "version": "7.5.9", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", @@ -22219,9 +22201,9 @@ } }, "node_modules/joi": { - "version": "17.12.2", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.2.tgz", - "integrity": "sha512-RonXAIzCiHLc8ss3Ibuz45u28GOsWE1UpfDXLbN/9NKbL4tCJf8TWYVKsoYuuh+sAUt7fsSNpA+r2+TBA6Wjmw==", + "version": "17.12.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.3.tgz", + "integrity": "sha512-2RRziagf555owrm9IRVtdKynOBeITiDpuZqIpgwqXShPncPKNiRQoiGsl/T8SQdq+8ugRzH2LqY67irr2y/d+g==", "peer": true, "dependencies": { "@hapi/hoek": "^9.3.0", @@ -24189,6 +24171,14 @@ "node": ">= 0.6" } }, + "node_modules/next-auth/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/next/node_modules/@next/env": { "version": "14.1.1", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.1.tgz", @@ -27231,6 +27221,14 @@ "node": ">=6.14.2" } }, + "node_modules/rpc-websockets/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -28624,9 +28622,9 @@ } }, "node_modules/terser": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.30.0.tgz", - "integrity": "sha512-Y/SblUl5kEyEFzhMAQdsxVHh+utAxd4IuRNJzKywY/4uzSogh3G219jqbDDxYu4MXO9CzY3tSEqmZvW6AoEDJw==", + "version": "5.30.3", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.30.3.tgz", + "integrity": "sha512-STdUgOUx8rLbMGO9IOwHLpCqolkDITFFQSMYYwKE1N2lY6MVSaeoi10z/EhWxRc6ybqoVmKSkhKYH/XUpl7vSA==", "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -29208,9 +29206,9 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, "node_modules/types-ramda": { - "version": "0.29.9", - "resolved": "https://registry.npmjs.org/types-ramda/-/types-ramda-0.29.9.tgz", - "integrity": "sha512-B+VbLtW68J4ncG/rccKaYDhlirKlVH/Izh2JZUfaPJv+3Tl2jbbgYsB1pvole1vXKSgaPlAe/wgEdOnMdAu52A==", + "version": "0.29.10", + "resolved": "https://registry.npmjs.org/types-ramda/-/types-ramda-0.29.10.tgz", + "integrity": "sha512-5PJiW/eiTPyXXBYGZOYGezMl6qj7keBiZheRwfjJZY26QPHsNrjfJnz0mru6oeqqoTHOni893Jfd6zyUXfQRWg==", "dev": true, "dependencies": { "ts-toolbelt": "^9.6.0" @@ -29663,9 +29661,13 @@ } }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { "uuid": "dist/bin/uuid" } @@ -30245,9 +30247,9 @@ } }, "node_modules/vite/node_modules/rollup": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.2.tgz", - "integrity": "sha512-MIlLgsdMprDBXC+4hsPgzWUasLO9CE4zOkj/u6j+Z6j5A4zRY+CtiXAdJyPtgCsc42g658Aeh1DlrdVEJhsL2g==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.0.tgz", + "integrity": "sha512-Qe7w62TyawbDzB4yt32R0+AbIo6m1/sqO7UPzFS8Z/ksL5mrfhA0v4CavfdmFav3D+ub4QeAgsGEe84DoWe/nQ==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -30260,21 +30262,21 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.13.2", - "@rollup/rollup-android-arm64": "4.13.2", - "@rollup/rollup-darwin-arm64": "4.13.2", - "@rollup/rollup-darwin-x64": "4.13.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.13.2", - "@rollup/rollup-linux-arm64-gnu": "4.13.2", - "@rollup/rollup-linux-arm64-musl": "4.13.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.13.2", - "@rollup/rollup-linux-riscv64-gnu": "4.13.2", - "@rollup/rollup-linux-s390x-gnu": "4.13.2", - "@rollup/rollup-linux-x64-gnu": "4.13.2", - "@rollup/rollup-linux-x64-musl": "4.13.2", - "@rollup/rollup-win32-arm64-msvc": "4.13.2", - "@rollup/rollup-win32-ia32-msvc": "4.13.2", - "@rollup/rollup-win32-x64-msvc": "4.13.2", + "@rollup/rollup-android-arm-eabi": "4.14.0", + "@rollup/rollup-android-arm64": "4.14.0", + "@rollup/rollup-darwin-arm64": "4.14.0", + "@rollup/rollup-darwin-x64": "4.14.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.14.0", + "@rollup/rollup-linux-arm64-gnu": "4.14.0", + "@rollup/rollup-linux-arm64-musl": "4.14.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.14.0", + "@rollup/rollup-linux-riscv64-gnu": "4.14.0", + "@rollup/rollup-linux-s390x-gnu": "4.14.0", + "@rollup/rollup-linux-x64-gnu": "4.14.0", + "@rollup/rollup-linux-x64-musl": "4.14.0", + "@rollup/rollup-win32-arm64-msvc": "4.14.0", + "@rollup/rollup-win32-ia32-msvc": "4.14.0", + "@rollup/rollup-win32-x64-msvc": "4.14.0", "fsevents": "~2.3.2" } }, @@ -31048,6 +31050,7 @@ "version": "4.1.5", "license": "Apache-2.0", "dependencies": { + "bignumber.js": "9.1.2", "dayjs": "1.11.10" } }, diff --git a/packages/common/index.ts b/packages/common/index.ts index cfcb94f0d5..5fdd619e7a 100644 --- a/packages/common/index.ts +++ b/packages/common/index.ts @@ -1,5 +1,6 @@ // -- Utils ------------------------------------------------------------------- export { DateUtil } from './src/utils/DateUtil.js' +export { NumberUtil } from './src/utils/NumberUtil.js' export { NetworkUtil } from './src/utils/NetworkUtil.js' export type * from './src/utils/TypeUtil.js' diff --git a/packages/common/package.json b/packages/common/package.json index 7c92d545b9..758a335c24 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -17,6 +17,7 @@ "lint": "eslint . --ext .js,.jsx,.ts,.tsx" }, "dependencies": { + "bignumber.js": "9.1.2", "dayjs": "1.11.10" }, "keywords": [ diff --git a/packages/common/src/utils/NumberUtil.ts b/packages/common/src/utils/NumberUtil.ts new file mode 100644 index 0000000000..436b950b89 --- /dev/null +++ b/packages/common/src/utils/NumberUtil.ts @@ -0,0 +1,49 @@ +import BigNumber from 'bignumber.js' + +export const NumberUtil = { + bigNumber(value: BigNumber.Value) { + return new BigNumber(value) + }, + + /** + * Format the given number or string to human readable numbers with the given number of decimals + * @param value - The value to format. It could be a number or string. If it's a string, it will be parsed to a float then formatted. + * @param decimals - number of decimals after dot + * @returns + */ + formatNumberToLocalString(value: string | number | undefined, decimals = 2) { + if (value === undefined) { + return '0.00' + } + + if (typeof value === 'number') { + return value.toLocaleString('en-US', { + maximumFractionDigits: decimals, + minimumFractionDigits: decimals + }) + } + + return parseFloat(value).toLocaleString('en-US', { + maximumFractionDigits: decimals, + minimumFractionDigits: decimals + }) + }, + + /** + * Multiply two numbers represented as strings with BigNumber to handle decimals correctly + * @param a string + * @param b string + * @returns + */ + multiply(a: BigNumber.Value | undefined, b: BigNumber.Value | undefined) { + if (a === undefined || b === undefined) { + // eslint-disable-next-line new-cap + return BigNumber(0) + } + + const aBigNumber = new BigNumber(a) + const bBigNumber = new BigNumber(b) + + return aBigNumber.multipliedBy(bBigNumber) + } +} diff --git a/packages/core/index.ts b/packages/core/index.ts index cfdf1f1323..3d3720acdb 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -55,6 +55,9 @@ export type { EventsControllerState } from './src/controllers/EventsController.j export { TransactionsController } from './src/controllers/TransactionsController.js' export type { TransactionsControllerState } from './src/controllers/TransactionsController.js' +export { ConvertController } from './src/controllers/ConvertController.js' +export type { ConvertControllerState } from './src/controllers/ConvertController.js' + export { SendController } from './src/controllers/SendController.js' export type { SendControllerState } from './src/controllers/SendController.js' diff --git a/packages/core/src/controllers/ConnectionController.ts b/packages/core/src/controllers/ConnectionController.ts index b54ed6a012..8fd178d112 100644 --- a/packages/core/src/controllers/ConnectionController.ts +++ b/packages/core/src/controllers/ConnectionController.ts @@ -2,7 +2,12 @@ import { subscribeKey as subKey } from 'valtio/vanilla/utils' import { proxy, ref } from 'valtio/vanilla' import { CoreHelperUtil } from '../utils/CoreHelperUtil.js' import { StorageUtil } from '../utils/StorageUtil.js' -import type { Connector, WcWallet } from '../utils/TypeUtil.js' +import type { + Connector, + EstimateGasTransactionArgs, + SendTransactionArgs, + WcWallet +} from '../utils/TypeUtil.js' import { TransactionsController } from './TransactionsController.js' // -- Types --------------------------------------------- // @@ -17,6 +22,10 @@ export interface ConnectionControllerClient { connectWalletConnect: (onUri: (uri: string) => void) => Promise disconnect: () => Promise signMessage: (message: string) => Promise + sendTransaction: (args: SendTransactionArgs) => Promise<`0x${string}` | null> + getEstimatedGas: (args: EstimateGasTransactionArgs) => Promise + parseUnits: (value: string, decimals: number) => bigint + formatUnits: (value: bigint, decimals: number) => string connectExternal?: (options: ConnectExternalOptions) => Promise checkInstalled?: (ids?: string[]) => boolean } @@ -83,6 +92,22 @@ export const ConnectionController = { return this._getClient().signMessage(message) }, + parseUnits(value: string, decimals: number) { + return this._getClient().parseUnits(value, decimals) + }, + + formatUnits(value: bigint, decimals: number) { + return this._getClient().formatUnits(value, decimals) + }, + + async sendTransaction(args: SendTransactionArgs) { + return this._getClient().sendTransaction(args) + }, + + async getEstimatedGas(args: EstimateGasTransactionArgs) { + return this._getClient().getEstimatedGas(args) + }, + checkInstalled(ids?: string[]) { return this._getClient().checkInstalled?.(ids) }, diff --git a/packages/core/src/controllers/ConvertController.ts b/packages/core/src/controllers/ConvertController.ts new file mode 100644 index 0000000000..b3a8f36292 --- /dev/null +++ b/packages/core/src/controllers/ConvertController.ts @@ -0,0 +1,656 @@ +import { subscribeKey as subKey } from 'valtio/utils' +import { proxy, subscribe as sub } from 'valtio/vanilla' +import { AccountController } from './AccountController.js' +import { ConstantsUtil } from '../utils/ConstantsUtil.js' +import { ConnectionController } from './ConnectionController.js' +import { ConvertApiUtil } from '../utils/ConvertApiUtil.js' +import { SnackController } from './SnackController.js' +import { RouterController } from './RouterController.js' +import { NumberUtil } from '@web3modal/common' + +const INITIAL_GAS_LIMIT = 150000 + +// -- Types --------------------------------------------- // +type TransactionParams = { + data: `0x${string}` + to: `0x${string}` + gas: bigint + gasPrice: bigint + value: bigint + toAmount: string +} + +class TransactionError extends Error { + shortMessage?: string + + constructor(message?: string, shortMessage?: string) { + super(message) + this.name = 'TransactionError' + this.shortMessage = shortMessage + } +} + +export interface ConvertControllerState { + // Loading states + initialized: boolean + loadingPrices: boolean + loading?: boolean + + // Approval & Convert transaction states + approvalTransaction: TransactionParams | undefined + convertTransaction: TransactionParams | undefined + transactionLoading?: boolean + transactionError?: string + + // Input values + sourceToken?: TokenInfo + sourceTokenAmount: string + sourceTokenPriceInUSD: number + toToken?: TokenInfo + toTokenAmount: string + toTokenPriceInUSD: number + networkPrice: string + networkBalanceInUSD: string + inputError: string | undefined + + // Request values + slippage: string + + // Tokens + tokens?: Record + suggestedTokens?: Record + popularTokens?: Record + foundTokens?: TokenInfo[] + myTokensWithBalance?: Record + tokensPriceMap: Record + + // Calculations + gasFee: bigint + gasPriceInUSD?: number + priceImpact: number | undefined + maxSlippage: number | undefined +} + +export interface TokenInfo { + address: `0x${string}` + symbol: string + name: string + decimals: number + logoURI: string + domainVersion?: string + eip2612?: boolean + isFoT?: boolean + tags?: string[] +} + +export interface TokenInfoWithPrice extends TokenInfo { + price: string +} + +export interface TokenInfoWithBalance extends TokenInfo { + balance: string + price: string +} + +type StateKey = keyof ConvertControllerState + +// -- State --------------------------------------------- // +const state = proxy({ + // Loading states + initialized: false, + loading: false, + loadingPrices: false, + + // Approval & Convert transaction states + approvalTransaction: undefined, + convertTransaction: undefined, + transactionError: undefined, + transactionLoading: false, + + // Input values + sourceToken: undefined, + sourceTokenAmount: '', + sourceTokenPriceInUSD: 0, + toToken: undefined, + toTokenAmount: '', + toTokenPriceInUSD: 0, + networkPrice: '0', + networkBalanceInUSD: '0', + inputError: undefined, + + // Request values + slippage: ConstantsUtil.CONVERT_SLIPPAGE_TOLERANCE, + + // Tokens + tokens: undefined, + popularTokens: undefined, + suggestedTokens: undefined, + foundTokens: undefined, + myTokensWithBalance: undefined, + tokensPriceMap: {}, + + // Calculations + gasFee: BigInt(0), + gasPriceInUSD: 0, + priceImpact: undefined, + maxSlippage: undefined +}) + +// -- Controller ---------------------------------------- // +export const ConvertController = { + state, + + subscribe(callback: (newState: ConvertControllerState) => void) { + return sub(state, () => callback(state)) + }, + + subscribeKey(key: K, callback: (value: ConvertControllerState[K]) => void) { + return subKey(state, key, callback) + }, + + getParams() { + const { address } = AccountController.state + + if (!address) { + throw new Error('No address found to swap the tokens from.') + } + + return { + fromAddress: address as `0x${string}`, + sourceTokenAddress: state.sourceToken?.address, + toTokenAddress: state.toToken?.address, + toTokenAmount: state.toTokenAmount, + toTokenDecimals: state.toToken?.decimals, + sourceTokenAmount: state.sourceTokenAmount, + sourceTokenDecimals: state.sourceToken?.decimals + } + }, + + setLoading(loading: boolean) { + ConvertController.state.loading = loading + }, + + setSourceToken(sourceToken: TokenInfo | undefined) { + if (!sourceToken) { + return + } + + state.sourceToken = sourceToken + this.setTokenValues(sourceToken.address, 'sourceToken') + }, + + setSourceTokenAmount(amount: string) { + const { sourceTokenAddress } = this.getParams() + + state.sourceTokenAmount = amount + + if (sourceTokenAddress) { + this.setTokenValues(sourceTokenAddress, 'sourceToken') + } + }, + + setToToken(toToken: TokenInfo | undefined) { + const { sourceTokenAddress, sourceTokenAmount } = this.getParams() + + if (!toToken) { + return + } + + state.toToken = toToken + this.setTokenValues(toToken.address, 'toToken') + + if (sourceTokenAddress && sourceTokenAmount) { + this.makeChecks() + } + }, + + setToTokenAmount(amount: string) { + const { toTokenAddress } = this.getParams() + + state.toTokenAmount = amount + + if (toTokenAddress) { + this.setTokenValues(toTokenAddress, 'toToken') + } + }, + + async setTokenValues(address: string, target: 'sourceToken' | 'toToken') { + const price = await this.getAddressPrice(address) + + if (target === 'sourceToken') { + state.sourceTokenPriceInUSD = price + } else if (target === 'toToken') { + state.toTokenPriceInUSD = price + } + }, + + switchTokens() { + const newSourceToken = state.toToken ? { ...state.toToken } : undefined + const newToToken = state.sourceToken ? { ...state.sourceToken } : undefined + + this.setSourceToken(newSourceToken) + this.setToToken(newToToken) + + this.setSourceTokenAmount(state.toTokenAmount || '0') + ConvertController.convertTokens() + }, + + resetTokens() { + state.tokens = undefined + state.popularTokens = undefined + state.myTokensWithBalance = undefined + state.initialized = false + }, + + resetValues() { + const networkToken = state.tokens?.[ConstantsUtil.NATIVE_TOKEN_ADDRESS] + this.setSourceToken(networkToken) + state.toToken = undefined + state.sourceTokenAmount = '0' + state.toTokenAmount = '0' + state.sourceTokenPriceInUSD = 0 + state.toTokenPriceInUSD = 0 + state.gasPriceInUSD = 0 + }, + + clearError() { + state.transactionError = undefined + }, + + async initializeState() { + if (!state.initialized) { + await this.getTokenList() + await this.getNetworkTokenPrice() + await this.getMyTokensWithBalance() + state.initialized = true + } + }, + + async getTokenList() { + const res = await ConvertApiUtil.getTokenList() + + state.tokens = res.tokens + state.popularTokens = Object.entries(res.tokens) + .sort(([, aTokenInfo], [, bTokenInfo]) => { + if (aTokenInfo.symbol < bTokenInfo.symbol) { + return -1 + } + if (aTokenInfo.symbol > bTokenInfo.symbol) { + return 1 + } + + return 0 + }) + .reduce>((limitedTokens, [tokenAddress, tokenInfo]) => { + if (ConstantsUtil.POPULAR_TOKENS.includes(tokenInfo.symbol)) { + limitedTokens[tokenAddress] = tokenInfo + } + + return limitedTokens + }, {}) + state.suggestedTokens = Object.entries(res.tokens).reduce>( + (limitedTokens, [tokenAddress, tokenInfo]) => { + if (ConstantsUtil.SUGGESTED_TOKENS.includes(tokenInfo.symbol)) { + limitedTokens[tokenAddress] = tokenInfo + } + + return limitedTokens + }, + {} + ) + const networkToken = res.tokens[ConstantsUtil.NATIVE_TOKEN_ADDRESS] + this.setSourceToken(networkToken) + + return state.tokens + }, + + async getAddressPrice(address: string) { + const existPrice = state.tokensPriceMap[address] + if (existPrice) { + return parseFloat(existPrice) + } + const prices = await ConvertApiUtil.getTokenPriceWithAddresses([address]) + const price = prices[address] || '0' + state.tokensPriceMap[address] = price + + return parseFloat(price) + }, + + async getNetworkTokenPrice() { + const prices = await ConvertApiUtil.getTokenPriceWithAddresses([ + ConstantsUtil.NATIVE_TOKEN_ADDRESS + ]) + const price = prices[ConstantsUtil.NATIVE_TOKEN_ADDRESS] || '0' + state.tokensPriceMap[ConstantsUtil.NATIVE_TOKEN_ADDRESS] = price + state.networkPrice = price + }, + + async getMyTokensWithBalance() { + const res = await ConvertApiUtil.getMyTokensWithBalance() + + if (!res) { + return + } + + await this.getInitialGasPrice() + + const networkToken = res[ConstantsUtil.NATIVE_TOKEN_ADDRESS] + + state.tokensPriceMap = Object.entries(res).reduce>( + (prices, [tokenAddress, tokenInfo]) => { + prices[tokenAddress] = tokenInfo.price + + return prices + }, + {} + ) + state.myTokensWithBalance = res + state.networkBalanceInUSD = networkToken + ? NumberUtil.multiply(networkToken.balance, networkToken.price).toString() + : '0' + }, + + async getInitialGasPrice() { + const res = await ConvertApiUtil.getGasPrice() + const instant = res.instant + const value = typeof instant === 'object' ? res.instant.maxFeePerGas : instant + const gasFee = BigInt(value) + const gasLimit = BigInt(INITIAL_GAS_LIMIT) + const gasPrice = this.calculateGasPriceInUSD(gasLimit, gasFee) + state.gasPriceInUSD = gasPrice + }, + + async refreshConvertValues() { + const { fromAddress, toTokenDecimals, toTokenAddress } = this.getParams() + + if (fromAddress && toTokenAddress && toTokenDecimals && !state.loading) { + const transaction = await this.getTransaction() + this.setTransactionDetails(transaction) + } + }, + + calculateGasPriceInEther(gas: bigint, gasPrice: bigint) { + const totalGasCostInWei = gasPrice * gas + const totalGasCostInEther = Number(totalGasCostInWei) / 1e18 + + return totalGasCostInEther + }, + + calculateGasPriceInUSD(gas: bigint, gasPrice: bigint) { + const totalGasCostInEther = this.calculateGasPriceInEther(gas, gasPrice) + const networkPriceInUSD = NumberUtil.bigNumber(state.networkPrice) + const gasCostInUSD = networkPriceInUSD.multipliedBy(totalGasCostInEther) + + return gasCostInUSD.toNumber() + }, + + calculatePriceImpact(toTokenAmount: string, gasPriceInUSD: number) { + const sourceTokenAmount = state.sourceTokenAmount + const sourceTokenPrice = state.sourceTokenPriceInUSD + const toTokenPrice = state.toTokenPriceInUSD + + const totalCostInUSD = NumberUtil.bigNumber(sourceTokenAmount) + .multipliedBy(sourceTokenPrice) + .plus(gasPriceInUSD) + const effectivePricePerToToken = totalCostInUSD.dividedBy(toTokenAmount) + const priceImpact = effectivePricePerToToken + .minus(toTokenPrice) + .dividedBy(toTokenPrice) + .multipliedBy(100) + + return priceImpact.toNumber() + }, + + calculateMaxSlippage() { + const slippageToleranceDecimal = NumberUtil.bigNumber(state.slippage).dividedBy(100) + const maxSlippageAmount = NumberUtil.multiply(state.sourceTokenAmount, slippageToleranceDecimal) + + return maxSlippageAmount.toNumber() + }, + + async convertTokens() { + const { sourceTokenAddress, toTokenAddress } = this.getParams() + + if (!sourceTokenAddress || !toTokenAddress) { + return + } + + await this.makeChecks() + }, + + async makeChecks() { + const { toTokenDecimals, toTokenAddress } = this.getParams() + + if (!toTokenDecimals || !toTokenAddress) { + return + } + + state.loading = true + const transaction = await this.getTransaction() + this.setTransactionDetails(transaction) + state.loading = false + }, + + async getTransaction() { + const { fromAddress, sourceTokenAddress, sourceTokenAmount, sourceTokenDecimals } = + this.getParams() + + if ( + !sourceTokenAddress || + !sourceTokenAmount || + parseFloat(sourceTokenAmount) === 0 || + !sourceTokenDecimals + ) { + return null + } + + const hasAllowance = await ConvertApiUtil.checkConvertAllowance({ + fromAddress, + sourceTokenAddress, + sourceTokenAmount, + sourceTokenDecimals + }) + + let transaction: TransactionParams | null = null + + if (hasAllowance) { + state.approvalTransaction = undefined + transaction = await this.createConvert() + state.convertTransaction = transaction || undefined + } else { + state.convertTransaction = undefined + transaction = await this.createTokenAllowance() + state.approvalTransaction = transaction + } + + return transaction + }, + + getToAmount() { + const { sourceTokenDecimals } = this.getParams() + const decimals = sourceTokenDecimals || 18 + const multiplyer = 10 ** decimals + + const toTokenConvertedAmount = + state.sourceTokenPriceInUSD && state.toTokenPriceInUSD && state.sourceTokenAmount + ? NumberUtil.bigNumber(state.sourceTokenAmount) + .multipliedBy(state.sourceTokenPriceInUSD) + .dividedBy(state.toTokenPriceInUSD) + : NumberUtil.bigNumber(0) + + return toTokenConvertedAmount.multipliedBy(multiplyer).toString() + }, + + async createTokenAllowance() { + const { fromAddress, sourceTokenAddress } = this.getParams() + + if (!sourceTokenAddress) { + throw new Error('>>> createTokenAllowance - No source token address found.') + } + + const transaction = await ConvertApiUtil.getConvertApprovalData({ + sourceTokenAddress + }) + + const gasLimit = await ConnectionController.getEstimatedGas({ + address: fromAddress, + to: transaction.to, + data: transaction.data + }) + + const toAmount = this.getToAmount() + + return { + ...transaction, + gas: gasLimit, + gasPrice: BigInt(transaction.gasPrice), + value: BigInt(transaction.value), + toAmount + } + }, + + async sendTransactionForApproval(data: TransactionParams) { + const { fromAddress } = this.getParams() + state.transactionLoading = true + + RouterController.pushTransactionStack({ + view: null, + goBack: true + }) + + try { + await ConnectionController.sendTransaction({ + address: fromAddress, + to: data.to, + data: data.data, + value: BigInt(data.value), + gasPrice: BigInt(data.gasPrice) + }) + + state.approvalTransaction = undefined + state.transactionLoading = false + this.makeChecks() + } catch (err) { + const error = err as TransactionError + state.transactionError = error?.shortMessage as unknown as string + state.transactionLoading = false + SnackController.showError(error?.shortMessage || 'Transaction error') + } + }, + + async createConvert() { + const { + fromAddress, + sourceTokenAddress, + sourceTokenDecimals, + sourceTokenAmount, + toTokenAddress + } = this.getParams() + + if (!sourceTokenAmount || !sourceTokenAddress || !toTokenAddress || !sourceTokenDecimals) { + return null + } + + try { + const response = await ConvertApiUtil.getConvertData({ + fromAddress, + sourceTokenAddress, + sourceTokenAmount, + toTokenAddress, + decimals: sourceTokenDecimals + }) + + const transaction = { + data: response.tx.data, + to: response.tx.to, + gas: BigInt(response.tx.gas), + gasPrice: BigInt(response.tx.gasPrice), + value: BigInt(response.tx.value), + toAmount: response.toAmount + } + + state.gasPriceInUSD = this.calculateGasPriceInUSD( + BigInt(response.tx.gas), + transaction.gasPrice + ) + + return transaction + } catch (error) { + return null + } + }, + + async sendTransactionForConvert(data: TransactionParams | undefined) { + if (!data) { + return undefined + } + + const { fromAddress } = this.getParams() + + state.transactionLoading = true + + RouterController.pushTransactionStack({ + view: 'Account', + goBack: false, + onSuccess() { + ConvertController.resetValues() + } + }) + + try { + const transactionHash = await ConnectionController.sendTransaction({ + address: fromAddress, + to: data.to, + data: data.data, + gas: data.gas, + gasPrice: BigInt(data.gasPrice), + value: data.value + }) + state.transactionLoading = false + + setTimeout(() => { + this.resetValues() + this.getMyTokensWithBalance() + }, 1000) + + return transactionHash + } catch (err) { + const error = err as TransactionError + state.transactionError = error?.shortMessage + state.transactionLoading = false + SnackController.showError(error?.shortMessage || 'Transaction error') + + return undefined + } + }, + + setTransactionDetails(transaction: TransactionParams | null) { + const { sourceTokenAddress, toTokenAddress, toTokenDecimals } = this.getParams() + + if (!transaction || !toTokenAddress || !toTokenDecimals) { + return + } + + const toTokenPrice = state.tokensPriceMap[toTokenAddress] || '0' + state.toTokenAmount = NumberUtil.bigNumber(transaction.toAmount) + .dividedBy(10 ** toTokenDecimals) + .toFixed(20) + state.toTokenPriceInUSD = NumberUtil.bigNumber(toTokenPrice).toNumber() + state.gasPriceInUSD = this.calculateGasPriceInUSD(transaction.gas, transaction.gasPrice) + + const isSourceTokenIsNetworkToken = sourceTokenAddress === ConstantsUtil.NATIVE_TOKEN_ADDRESS + const totalNativeTokenCostInUSD = isSourceTokenIsNetworkToken + ? NumberUtil.bigNumber(state.sourceTokenPriceInUSD).plus(state.gasPriceInUSD) + : state.gasPriceInUSD + const insufficientBalance = NumberUtil.bigNumber(totalNativeTokenCostInUSD).isGreaterThan( + state.networkBalanceInUSD + ) + + if (insufficientBalance) { + state.inputError = insufficientBalance ? 'Insufficient balance' : undefined + } + + state.priceImpact = this.calculatePriceImpact(state.toTokenAmount, state.gasPriceInUSD) + state.maxSlippage = this.calculateMaxSlippage() + } +} diff --git a/packages/core/src/controllers/RouterController.ts b/packages/core/src/controllers/RouterController.ts index 3dc2fa8869..168025a416 100644 --- a/packages/core/src/controllers/RouterController.ts +++ b/packages/core/src/controllers/RouterController.ts @@ -3,6 +3,13 @@ import { proxy } from 'valtio/vanilla' import type { CaipNetwork, Connector, WcWallet } from '../utils/TypeUtil.js' // -- Types --------------------------------------------- // +type TransactionAction = { + goBack: boolean + view: RouterControllerState['view'] | null + close?: boolean + onSuccess?: () => void + onCancel?: () => void +} export interface RouterControllerState { view: | 'Account' @@ -39,6 +46,9 @@ export interface RouterControllerState { | 'WhatIsANetwork' | 'WhatIsAWallet' | 'WhatIsABuy' + | 'Convert' + | 'ConvertSelectToken' + | 'ConvertPreview' history: RouterControllerState['view'][] data?: { connector?: Connector @@ -46,13 +56,16 @@ export interface RouterControllerState { network?: CaipNetwork email?: string newEmail?: string + target?: 'sourceToken' | 'toToken' } + transactionStack: TransactionAction[] } // -- State --------------------------------------------- // const state = proxy({ view: 'Connect', - history: ['Connect'] + history: ['Connect'], + transactionStack: [] }) type StateKey = keyof RouterControllerState @@ -65,6 +78,30 @@ export const RouterController = { return subKey(state, key, callback) }, + pushTransactionStack(action: TransactionAction) { + state.transactionStack.push(action) + }, + + popTransactionStack(cancel?: boolean) { + const action = state.transactionStack.pop() + + if (!action) { + return + } + + if (cancel) { + this.goBack() + action?.onCancel?.() + } else { + if (action.goBack) { + this.goBack() + } else if (action.view) { + this.reset(action.view) + } + action?.onSuccess?.() + } + }, + push(view: RouterControllerState['view'], data?: RouterControllerState['data']) { if (view !== state.view) { state.view = view diff --git a/packages/core/src/utils/ConstantsUtil.ts b/packages/core/src/utils/ConstantsUtil.ts index 52714f1064..8aa5412370 100644 --- a/packages/core/src/utils/ConstantsUtil.ts +++ b/packages/core/src/utils/ConstantsUtil.ts @@ -61,7 +61,101 @@ export const ConstantsUtil = { Base: 'base' }, - WC_COINBASE_ONRAMP_APP_ID: 'bf18c88d-495a-463b-b249-0b9d3656cf5e' + WC_COINBASE_ONRAMP_APP_ID: 'bf18c88d-495a-463b-b249-0b9d3656cf5e', + + SUGGESTED_TOKENS: [ + 'ETH', + 'UNI', + '1INCH', + 'AAVE', + 'SOL', + 'ADA', + 'AVAX', + 'DOT', + 'LINK', + 'NITRO', + 'GAIA', + 'MILK', + 'TRX', + 'NEAR', + 'GNO', + 'WBTC', + 'DAI', + 'WETH', + 'USDC', + 'USDT', + 'ARB', + 'BAL', + 'BICO', + 'CRV', + 'ENS', + 'MATIC', + 'OP' + ], + + POPULAR_TOKENS: [ + 'ETH', + 'UNI', + '1INCH', + 'AAVE', + 'SOL', + 'ADA', + 'AVAX', + 'DOT', + 'LINK', + 'NITRO', + 'GAIA', + 'MILK', + 'TRX', + 'NEAR', + 'GNO', + 'WBTC', + 'DAI', + 'WETH', + 'USDC', + 'USDT', + 'ARB', + 'BAL', + 'BICO', + 'CRV', + 'ENS', + 'MATIC', + 'OP', + // Some Polygon tokens + 'DAI', + 'CHAMP', + 'WOLF', + 'SALE', + 'BAL', + 'BUSD', + 'MUST', + 'BTCpx', + 'ROUTE', + 'HEX', + 'WELT', + 'amDAI', + 'VSQ', + 'VISION', + 'AURUM', + 'pSP', + 'SNX', + 'VC', + 'LINK', + 'CHP', + 'amUSDT', + 'SPHERE', + 'FOX', + 'GIDDY', + 'GFC', + 'OMEN', + 'OX_OLD', + 'DE', + 'WNT' + ], + + NATIVE_TOKEN_ADDRESS: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + + CONVERT_SLIPPAGE_TOLERANCE: '0.5' } export type CoinbasePaySDKChainNameValues = diff --git a/packages/core/src/utils/ConvertApiUtil.ts b/packages/core/src/utils/ConvertApiUtil.ts new file mode 100644 index 0000000000..66a9efe6a7 --- /dev/null +++ b/packages/core/src/utils/ConvertApiUtil.ts @@ -0,0 +1,330 @@ +import { CoreHelperUtil } from '../utils/CoreHelperUtil.js' +import { NetworkController } from '../controllers/NetworkController.js' +import { FetchUtil } from '../utils/FetchUtil.js' +import { AccountController } from '../controllers/AccountController.js' +import { ConnectionController } from '../controllers/ConnectionController.js' +import { ConstantsUtil } from '../utils/ConstantsUtil.js' + +const ONEINCH_API_BASE_URL = 'https://1inch-swap-proxy.walletconnect-v1-bridge.workers.dev' + +function get1InchEndpoints(chainId: number, address: string | undefined) { + return { + approveTransaction: `/swap/v5.2/${chainId}/approve/transaction`, + approveAllowance: `/swap/v5.2/${chainId}/approve/allowance`, + gas: `/gas-price/v1.5/${chainId}`, + gasPrice: `/gas-price/v1.5/${chainId}`, + swap: `/swap/v5.2/${chainId}/swap`, + tokens: `/swap/v5.2/${chainId}/tokens`, + tokensCustom: `/token/v1.2/${chainId}/custom`, + tokensPrices: `/price/v1.1/${chainId}`, + search: `/token/v1.2/${chainId}/search`, + balance: `/balance/v1.2/${chainId}/balances/${address}`, + quote: `/swap/v6.0/${chainId}/quote` + } +} + +// -- Types --------------------------------------------- // +export type TokenInfo = { + address: `0x${string}` + symbol: string + name: string + decimals: number + logoURI: string + domainVersion?: string + eip2612?: boolean + isFoT?: boolean + tags?: string[] +} + +export interface TokenInfoWithPrice extends TokenInfo { + price: string +} + +export interface TokenInfoWithBalance extends TokenInfo { + balance: string + price: string +} + +export type SwapApprovalData = { + data: `0x${string}` + to: `0x${string}` + gasPrice: string + value: string +} + +export type TokenList = { + tokens: Record +} + +export type GetAllowanceParams = { + fromAddress: string + sourceTokenAddress: string + sourceTokenAmount: string + sourceTokenDecimals: number +} + +export type GetApprovalParams = { + sourceTokenAddress: string + sourceTokenAmount?: string +} + +export type GetConvertDataParams = { + sourceTokenAddress: string + toTokenAddress: string + sourceTokenAmount: string + fromAddress: string + decimals: number +} + +export type TransactionData = { + from: string + to: `0x${string}` + data: `0x${string}` + value: string + gas: bigint + gasPrice: string +} + +export type GetConvertDataResponse = { + toAmount: string + tx: TransactionData +} + +export type GetGasPricesResponse = { + baseFree: string + low: { + maxPriorityFeePerGas: string + maxFeePerGas: string + } + medium: { + maxPriorityFeePerGas: string + maxFeePerGas: string + } + high: { + maxPriorityFeePerGas: string + maxFeePerGas: string + } + instant: { + maxPriorityFeePerGas: string + maxFeePerGas: string + } +} + +// -- Controller ---------------------------------------- // +export const ConvertApiUtil = { + get1InchAPI() { + const api = new FetchUtil({ baseUrl: ONEINCH_API_BASE_URL }) + const chainId = CoreHelperUtil.getEvmChainId(NetworkController.state.caipNetwork?.id) + const { address } = AccountController.state + + const endpoints = get1InchEndpoints(chainId, address) + + return { + api, + paths: { + approveTransaction: endpoints.approveTransaction, + approveAllowance: endpoints.approveAllowance, + gas: endpoints.gasPrice, + gasPrice: endpoints.gasPrice, + swap: endpoints.swap, + tokens: endpoints.tokens, + tokensCustom: endpoints.tokensCustom, + tokenPrices: endpoints.tokensPrices, + search: endpoints.search, + balance: endpoints.balance, + quote: endpoints.quote + } + } + }, + + async getGasPrice() { + const { api, paths } = this.get1InchAPI() + + const gasPrices = await api.get({ + path: paths.gasPrice, + headers: { 'content-type': 'application/json' } + }) + + return gasPrices + }, + + async checkConvertAllowance({ + fromAddress, + sourceTokenAddress, + sourceTokenAmount, + sourceTokenDecimals + }: GetAllowanceParams) { + const { api, paths } = this.get1InchAPI() + + const res = await api.get<{ allowance: string }>({ + path: paths.approveAllowance, + params: { tokenAddress: sourceTokenAddress, walletAddress: fromAddress } + }) + + if (res?.allowance && sourceTokenAmount && sourceTokenDecimals) { + const parsedValue = ConnectionController.parseUnits(sourceTokenAmount, sourceTokenDecimals) + const hasAllowance = BigInt(res.allowance) >= parsedValue + + return hasAllowance + } + + return false + }, + + async getTokenList() { + const { api, paths } = this.get1InchAPI() + + return await api.get({ path: paths.tokens }) + }, + + async searchTokens(searchTerm: string) { + const { api, paths } = this.get1InchAPI() + + return await api.get({ + path: paths.search, + params: { query: searchTerm } + }) + }, + + async getMyTokensWithBalance() { + const { balances, tokenAddresses } = await this.getBalances() + + if (!tokenAddresses?.length) { + return undefined + } + + const addresses = [...tokenAddresses, ConstantsUtil.NATIVE_TOKEN_ADDRESS] + + const [tokenInfos, tokensPrices] = await Promise.all([ + this.getTokenInfoWithAddresses(addresses), + this.getTokenPriceWithAddresses(addresses) + ]) + + const mergedTokensWithBalances = this.mergeTokensWithBalanceAndPrice( + tokenInfos, + balances, + tokensPrices + ) + + return mergedTokensWithBalances + }, + + async getBalances() { + const { api, paths } = this.get1InchAPI() + + const balances = await api.get>({ + path: paths.balance + }) + + const nonEmptyBalances = Object.entries(balances).reduce>( + (filteredBalances, [tokenAddress, balance]) => { + if (balance !== '0') { + filteredBalances[tokenAddress] = balance + } + + return filteredBalances + }, + {} + ) + + return { balances: nonEmptyBalances, tokenAddresses: Object.keys(nonEmptyBalances) } + }, + + async getTokenInfoWithAddresses(addresses: string[]) { + const { api, paths } = this.get1InchAPI() + + return api.get>({ + path: paths.tokensCustom, + params: { addresses: addresses.join(',') } + }) + }, + + async getTokenPriceWithAddresses(addresses: string[]) { + const { api, paths } = this.get1InchAPI() + + return await api.post>({ + path: paths.tokenPrices, + body: { tokens: addresses, currency: 'USD' }, + headers: { + 'content-type': 'application/json' + } + }) + }, + + mergeTokensWithBalanceAndPrice( + tokens: Record, + balances: Record, + tokensPrice: Record + ) { + const mergedTokens = Object.entries(tokens).reduce>( + (_mergedTokens, [tokenAddress, tokenInfo]) => { + _mergedTokens[tokenAddress] = { + ...tokenInfo, + balance: ConnectionController.formatUnits( + BigInt(balances[tokenAddress] ?? '0'), + tokenInfo.decimals + ), + price: tokensPrice[tokenAddress] ?? '0' + } + + return _mergedTokens + }, + {} + ) + + return mergedTokens + }, + + async getConvertData({ + sourceTokenAddress, + toTokenAddress, + sourceTokenAmount, + fromAddress, + decimals = 9 + }: GetConvertDataParams): Promise { + const { api, paths } = this.get1InchAPI() + + return await api.get({ + path: paths.swap, + params: { + src: sourceTokenAddress, + dst: toTokenAddress, + slippage: ConstantsUtil.CONVERT_SLIPPAGE_TOLERANCE, + from: fromAddress, + amount: ConnectionController.parseUnits(sourceTokenAmount, decimals).toString() + } + }) + }, + + async getConvertApprovalData({ sourceTokenAddress }: GetApprovalParams) { + const { api, paths } = this.get1InchAPI() + + return await api.get({ + path: paths.approveTransaction, + params: { + tokenAddress: sourceTokenAddress + } + }) + }, + + async getQuoteApprovalData({ + sourceTokenAddress, + toTokenAddress, + sourceTokenAmount, + fromAddress, + decimals = 9 + }: GetConvertDataParams): Promise { + const { api, paths } = this.get1InchAPI() + + return await api.get({ + path: paths.quote, + params: { + src: sourceTokenAddress, + dst: toTokenAddress, + slippage: ConstantsUtil.CONVERT_SLIPPAGE_TOLERANCE, + from: fromAddress, + amount: ConnectionController.parseUnits(sourceTokenAmount, decimals).toString() + } + }) + } +} diff --git a/packages/core/src/utils/CoreHelperUtil.ts b/packages/core/src/utils/CoreHelperUtil.ts index 55131516ad..e5a99c1f57 100644 --- a/packages/core/src/utils/CoreHelperUtil.ts +++ b/packages/core/src/utils/CoreHelperUtil.ts @@ -54,6 +54,16 @@ export const CoreHelperUtil = { return caipAddress.split(':')[2] }, + getEvmChainId(caipNetworkId?: `${string}:${string}`) { + const strChainId = caipNetworkId?.split(':')?.[1] + if (!strChainId) { + // Default to Ethereum mainnet + return 1 + } + + return parseInt(strChainId, 10) + }, + async wait(milliseconds: number) { return new Promise(resolve => { setTimeout(resolve, milliseconds) @@ -220,6 +230,7 @@ export const CoreHelperUtil = { return 'Unknown error' }, + sortRequestedNetworks( approvedIds: `${string}:${string}`[] | undefined, requestedNetworks: CaipNetwork[] = [] @@ -249,6 +260,7 @@ export const CoreHelperUtil = { return requestedNetworks }, + calculateBalance(array: Balance[]) { let sum = 0 for (const item of array) { @@ -257,12 +269,14 @@ export const CoreHelperUtil = { return sum }, + formatTokenBalance(number: number) { const roundedNumber = number.toFixed(2) const [dollars, pennies] = roundedNumber.split('.') return { dollars, pennies } }, + isAddress(address: string): boolean { if (!/^(?:0x)?[0-9a-f]{40}$/iu.test(address)) { return false diff --git a/packages/core/src/utils/TypeUtil.ts b/packages/core/src/utils/TypeUtil.ts index d58952ecb1..796eebae68 100644 --- a/packages/core/src/utils/TypeUtil.ts +++ b/packages/core/src/utils/TypeUtil.ts @@ -339,6 +339,18 @@ export type Event = network: string } } + | { + type: 'track' + event: 'CLICK_CONVERT' + } + | { + type: 'track' + event: 'CLICK_SELECT_TOKEN_TO_SWAP' + } + | { + type: 'track' + event: 'CLICK_SELECT_NETWORK_TO_SWAP' + } // Onramp Types export type DestinationWallet = { @@ -400,3 +412,18 @@ export type GetQuoteArgs = { amount: string network: string } + +export interface SendTransactionArgs { + to: `0x${string}` + data: `0x${string}` + value: bigint + gas?: bigint + gasPrice: bigint + address: `0x${string}` +} + +export interface EstimateGasTransactionArgs { + address: `0x${string}` + to: `0x${string}` + data: `0x${string}` +} diff --git a/packages/core/tests/controllers/ConnectionController.test.ts b/packages/core/tests/controllers/ConnectionController.test.ts index bf55b7b40a..2090363284 100644 --- a/packages/core/tests/controllers/ConnectionController.test.ts +++ b/packages/core/tests/controllers/ConnectionController.test.ts @@ -15,8 +15,12 @@ const client: ConnectionControllerClient = { }, disconnect: async () => Promise.resolve(), signMessage: async (message: string) => Promise.resolve(message), + getEstimatedGas: async () => Promise.resolve(BigInt(0)), connectExternal: async _id => Promise.resolve(), - checkInstalled: _id => true + checkInstalled: _id => true, + parseUnits: value => BigInt(value), + formatUnits: value => value.toString(), + sendTransaction: () => Promise.resolve('0x') } const clientConnectExternalSpy = vi.spyOn(client, 'connectExternal') @@ -25,7 +29,11 @@ const clientCheckInstalledSpy = vi.spyOn(client, 'checkInstalled') const partialClient: ConnectionControllerClient = { connectWalletConnect: async () => Promise.resolve(), disconnect: async () => Promise.resolve(), - signMessage: async (message: string) => Promise.resolve(message) + getEstimatedGas: async () => Promise.resolve(BigInt(0)), + signMessage: async (message: string) => Promise.resolve(message), + parseUnits: value => BigInt(value), + formatUnits: value => value.toString(), + sendTransaction: () => Promise.resolve('0x') } // -- Tests -------------------------------------------------------------------- diff --git a/packages/core/tests/controllers/ConvertController.test.ts b/packages/core/tests/controllers/ConvertController.test.ts new file mode 100644 index 0000000000..52c6841da5 --- /dev/null +++ b/packages/core/tests/controllers/ConvertController.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from 'vitest' +import { ConvertController } from '../../index.js' + +// -- Tests -------------------------------------------------------------------- +describe('ConvertController', () => { + it('should have default state as expected', () => { + expect(ConvertController.state.initialized).toEqual(false) + expect(ConvertController.state.tokens).toEqual([]) + expect(ConvertController.state.sourceToken).toEqual(undefined) + expect(ConvertController.state.toToken).toEqual(undefined) + }) +}) diff --git a/packages/ethers/src/client.ts b/packages/ethers/src/client.ts index 655823f0aa..a8ea08a1d2 100644 --- a/packages/ethers/src/client.ts +++ b/packages/ethers/src/client.ts @@ -8,6 +8,7 @@ import type { LibraryOptions, NetworkControllerClient, PublicStateControllerState, + SendTransactionArgs, Token } from '@web3modal/scaffold' import { Web3ModalScaffold } from '@web3modal/scaffold' @@ -26,7 +27,9 @@ import { formatEther, JsonRpcProvider, InfuraProvider, - getAddress as getOriginalAddress + getAddress as getOriginalAddress, + parseUnits, + formatUnits } from 'ethers' import { EthersConstantsUtil, @@ -37,6 +40,8 @@ import type { EthereumProviderOptions } from '@walletconnect/ethereum-provider' import type { Eip1193Provider } from 'ethers' import { W3mFrameProvider, W3mFrameHelpers, W3mFrameRpcConstants } from '@web3modal/wallet' import type { CombinedProvider } from '@web3modal/scaffold-utils/ethers' +import { BrowserProvider } from 'ethers' +import { JsonRpcSigner } from 'ethers' import { NetworkUtil } from '@web3modal/common' // -- Types --------------------------------------------------------------------- @@ -273,6 +278,67 @@ export class Web3Modal extends Web3ModalScaffold { }) return signature as `0x${string}` + }, + + parseUnits: (value: string, decimals: number) => parseUnits(value, decimals), + + formatUnits: (value: bigint, decimals: number) => formatUnits(value, decimals), + + async getEstimatedGas(data) { + const chainId = EthersStoreUtil.state.chainId + const provider = EthersStoreUtil.state.provider + const address = EthersStoreUtil.state.address + + if (!provider) { + throw new Error('connectionControllerClient:sendTransaction - provider is undefined') + } + + if (!address) { + throw new Error('connectionControllerClient:sendTransaction - address is undefined') + } + + const txParams = { + from: data.address, + to: data.to, + data: data.data, + type: 0 + } + + const browserProvider = new BrowserProvider(provider, chainId) + const signer = new JsonRpcSigner(browserProvider, address) + const gas = await signer.estimateGas(txParams) + + return gas + }, + + sendTransaction: async (data: SendTransactionArgs) => { + const chainId = EthersStoreUtil.state.chainId + const provider = EthersStoreUtil.state.provider + const address = EthersStoreUtil.state.address + + if (!provider) { + throw new Error('connectionControllerClient:sendTransaction - provider is undefined') + } + + if (!address) { + throw new Error('connectionControllerClient:sendTransaction - address is undefined') + } + + const txParams = { + to: data.to, + value: data.value, + gasLimit: data.gas, + gasPrice: data.gasPrice, + data: data.data, + type: 0 + } + + const browserProvider = new BrowserProvider(provider, chainId) + const signer = new JsonRpcSigner(browserProvider, address) + const txResponse = await signer.sendTransaction(txParams) + const txReceipt = await txResponse.wait() + + return (txReceipt?.hash as `0x${string}`) || null } } diff --git a/packages/ethers5/src/client.ts b/packages/ethers5/src/client.ts index 0202fa8407..8330e02a30 100644 --- a/packages/ethers5/src/client.ts +++ b/packages/ethers5/src/client.ts @@ -8,6 +8,7 @@ import type { LibraryOptions, NetworkControllerClient, PublicStateControllerState, + SendTransactionArgs, Token } from '@web3modal/scaffold' import { Web3ModalScaffold } from '@web3modal/scaffold' @@ -257,6 +258,41 @@ export class Web3Modal extends Web3ModalScaffold { }) return signature as `0x${string}` + }, + + parseUnits: (value: string, decimals: number) => + ethers.utils.parseUnits(value, decimals).toBigInt(), + + formatUnits: (value: bigint, decimals: number) => ethers.utils.formatUnits(value, decimals), + + sendTransaction: async (data: SendTransactionArgs) => { + const provider = EthersStoreUtil.state.provider + const address = EthersStoreUtil.state.address + + if (!provider) { + throw new Error('connectionControllerClient:sendTransaction - provider is undefined') + } + + if (!address) { + throw new Error('connectionControllerClient:sendTransaction - address is undefined') + } + + const txParams = { + to: data.to, + value: data.value, + gasLimit: data.gas, + gasPrice: data.gasPrice, + data: data.data, + type: 0 + } + + const browserProvider = new ethers.providers.Web3Provider(provider) + const signer = browserProvider.getSigner() + + const txResponse = await signer.sendTransaction(txParams) + const txReceipt = await txResponse.wait() + + return (txReceipt?.blockHash as `0x${string}`) || null } } diff --git a/packages/scaffold/index.ts b/packages/scaffold/index.ts index 0d3f93cd92..485a75a5c5 100644 --- a/packages/scaffold/index.ts +++ b/packages/scaffold/index.ts @@ -21,6 +21,10 @@ export * from './src/views/w3m-onramp-activity-view/index.js' export * from './src/views/w3m-onramp-fiat-select-view/index.js' export * from './src/views/w3m-onramp-providers-view/index.js' export * from './src/views/w3m-onramp-tokens-select-view/index.js' +export * from './src/views/w3m-convert-view/index.js' +export * from './src/views/w3m-convert-preview-view/index.js' +export * from './src/views/w3m-convert-select-token-view/index.js' +export * from './src/views/w3m-convert-view/index.js' export * from './src/views/w3m-transactions-view/index.js' export * from './src/views/w3m-what-is-a-network-view/index.js' export * from './src/views/w3m-what-is-a-wallet-view/index.js' diff --git a/packages/scaffold/src/client.ts b/packages/scaffold/src/client.ts index 3dd28bdf2f..48f024f153 100644 --- a/packages/scaffold/src/client.ts +++ b/packages/scaffold/src/client.ts @@ -9,7 +9,8 @@ import type { ThemeMode, ThemeVariables, ModalControllerState, - ConnectedWalletInfo + ConnectedWalletInfo, + RouterControllerState } from '@web3modal/core' import { AccountController, @@ -23,7 +24,8 @@ import { OptionsController, PublicStateController, ThemeController, - SnackController + SnackController, + RouterController } from '@web3modal/core' import { setColorTheme, setThemeVariables } from '@web3modal/ui' import type { SIWEControllerClient } from '@web3modal/siwe' @@ -83,6 +85,22 @@ export class Web3ModalScaffold { ModalController.close() } + public redirect(route: RouterControllerState['view']) { + RouterController.push(route) + } + + public popTransactionStack(cancel?: boolean) { + RouterController.popTransactionStack(cancel) + } + + public isOpen() { + return ModalController.state.open + } + + public isTransactionStackEmpty() { + return RouterController.state.transactionStack.length === 0 + } + public setLoading(loading: ModalControllerState['loading']) { ModalController.setLoading(loading) } diff --git a/packages/scaffold/src/modal/w3m-router/index.ts b/packages/scaffold/src/modal/w3m-router/index.ts index d9b0c29978..44928a9a43 100644 --- a/packages/scaffold/src/modal/w3m-router/index.ts +++ b/packages/scaffold/src/modal/w3m-router/index.ts @@ -117,6 +117,12 @@ export class W3mRouter extends LitElement { return html`` case 'WalletCompatibleNetworks': return html`` + case 'Convert': + return html`` + case 'ConvertSelectToken': + return html`` + case 'ConvertPreview': + return html`` case 'WalletSend': return html`` case 'WalletSendSelectToken': diff --git a/packages/scaffold/src/partials/w3m-account-default-widget/index.ts b/packages/scaffold/src/partials/w3m-account-default-widget/index.ts index 8703f0af4b..05beb73d4d 100644 --- a/packages/scaffold/src/partials/w3m-account-default-widget/index.ts +++ b/packages/scaffold/src/partials/w3m-account-default-widget/index.ts @@ -145,6 +145,15 @@ export class W3mAccountDefaultWidget extends LitElement { > Activity + + Convert + void)[] = [] // -- State & Properties -------------------------------- // @@ -23,14 +25,12 @@ export class W3mAccountTokensWidget extends LitElement { }) ] ) + this.watchConvertValues() } public override disconnectedCallback() { this.unsubscribe.forEach(unsubscribe => unsubscribe()) - } - - public override firstUpdated() { - AccountController.fetchTokenBalance() + clearInterval(this.watchTokenBalance) } // -- Render -------------------------------------------- // @@ -39,6 +39,10 @@ export class W3mAccountTokensWidget extends LitElement { } // -- Private ------------------------------------------- // + private watchConvertValues() { + this.watchTokenBalance = setInterval(() => AccountController.fetchTokenBalance(), 1000) + } + private tokenTemplate() { if (this.tokenBalance && this.tokenBalance?.length > 0) { return html` diff --git a/packages/scaffold/src/partials/w3m-account-wallet-features-widget/index.ts b/packages/scaffold/src/partials/w3m-account-wallet-features-widget/index.ts index 047b8dbfd1..44d7b824ab 100644 --- a/packages/scaffold/src/partials/w3m-account-wallet-features-widget/index.ts +++ b/packages/scaffold/src/partials/w3m-account-wallet-features-widget/index.ts @@ -93,7 +93,11 @@ export class W3mAccountWalletFeaturesWidget extends LitElement { text="Buy" icon="card" > - + + return html` ${this.selectedCurrency ? html` ModalController.open({ view: `OnRamp${this.type}Select` })} > - ${this.selectedCurrency.symbol} + ${this.selectedCurrency.symbol} ` : html``} ` diff --git a/packages/scaffold/src/views/w3m-approve-transaction-view/index.ts b/packages/scaffold/src/views/w3m-approve-transaction-view/index.ts index 76384652c8..5d96399bc2 100644 --- a/packages/scaffold/src/views/w3m-approve-transaction-view/index.ts +++ b/packages/scaffold/src/views/w3m-approve-transaction-view/index.ts @@ -2,7 +2,17 @@ import { customElement } from '@web3modal/ui' import { LitElement, html } from 'lit' import { state } from 'lit/decorators.js' import styles from './styles.js' -import { ModalController, ConnectorController, ThemeController } from '@web3modal/core' +import { + ModalController, + ConnectorController, + ThemeController, + RouterController +} from '@web3modal/core' + +// -- Variables ------------------------------------------- // +const PAGE_HEIGHT = 400 +const PAGE_WIDTH = 360 +const HEADER_HEIGHT = 64 @customElement('w3m-approve-transaction-view') export class W3mApproveTransactionView extends LitElement { @@ -20,34 +30,34 @@ export class W3mApproveTransactionView extends LitElement { public constructor() { super() + this.unsubscribe.push( - ModalController.subscribeKey('open', val => { - if (!val) { - this.onHideIframe() - } - }) + ...[ + ModalController.subscribeKey('open', isOpen => { + if (!isOpen) { + this.onHideIframe() + RouterController.popTransactionStack() + } + }) + ] ) } public override disconnectedCallback() { + this.onHideIframe() this.unsubscribe.forEach(unsubscribe => unsubscribe()) this.bodyObserver?.unobserve(window.document.body) } public override async firstUpdated() { - const verticalPadding = 10 - await this.syncTheme() this.iframe.style.display = 'block' - const blueprint = this.renderRoot.querySelector('div') this.bodyObserver = new ResizeObserver(() => { - const data = blueprint?.getBoundingClientRect() - const dimensions = data ?? { left: 0, top: 0, width: 0, height: 0 } - this.iframe.style.width = `360px` - this.iframe.style.height = `${dimensions.height - verticalPadding}px` - this.iframe.style.left = 'calc(50% - 180px)' - this.iframe.style.top = `${dimensions.top + verticalPadding / 2}px` + this.iframe.style.width = `${PAGE_WIDTH}px` + this.iframe.style.height = `${PAGE_HEIGHT}px` + this.iframe.style.left = `calc(50% - ${PAGE_WIDTH / 2}px)` + this.iframe.style.top = `calc(50% - ${PAGE_HEIGHT / 2}px + ${HEADER_HEIGHT / 2}px)` this.ready = true }) this.bodyObserver.observe(window.document.body) @@ -75,12 +85,12 @@ export class W3mApproveTransactionView extends LitElement { } private async onHideIframe() { + this.iframe.style.display = 'none' await this.iframe.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 200, easing: 'ease', fill: 'forwards' }).finished - this.iframe.style.display = 'none' } private async syncTheme() { diff --git a/packages/scaffold/src/views/w3m-convert-preview-view/index.ts b/packages/scaffold/src/views/w3m-convert-preview-view/index.ts new file mode 100644 index 0000000000..ef6775d60e --- /dev/null +++ b/packages/scaffold/src/views/w3m-convert-preview-view/index.ts @@ -0,0 +1,228 @@ +import { customElement, formatNumberToLocalString } from '@web3modal/ui' +import { LitElement, html } from 'lit' +import styles from './styles.js' +import { + AccountController, + NetworkController, + RouterController, + ConvertController +} from '@web3modal/core' +import { state } from 'lit/decorators.js' + +@customElement('w3m-convert-preview-view') +export class W3mConvertPreviewView extends LitElement { + public static override styles = styles + + private unsubscribe: ((() => void) | undefined)[] = [] + + // -- State & Properties -------------------------------- // + @state() private detailsOpen = true + + @state() private approvalTransaction = ConvertController.state.approvalTransaction + + @state() private convertTransaction = ConvertController.state.convertTransaction + + @state() private sourceToken = ConvertController.state.sourceToken + + @state() private sourceTokenAmount = ConvertController.state.sourceTokenAmount ?? '' + + @state() private sourceTokenPriceInUSD = ConvertController.state.sourceTokenPriceInUSD + + @state() private toToken = ConvertController.state.toToken + + @state() private toTokenAmount = ConvertController.state.toTokenAmount ?? '' + + @state() private toTokenPriceInUSD = ConvertController.state.toTokenPriceInUSD + + @state() private caipNetwork = NetworkController.state.caipNetwork + + @state() private transactionLoading = ConvertController.state.transactionLoading + + @state() private balanceSymbol = AccountController.state.balanceSymbol + + @state() private gasPriceInUSD = ConvertController.state.gasPriceInUSD + + @state() private priceImpact = ConvertController.state.priceImpact + + @state() private maxSlippage = ConvertController.state.maxSlippage + + // -- Lifecycle ----------------------------------------- // + public constructor() { + super() + + this.unsubscribe.push( + ...[ + AccountController.subscribeKey('balanceSymbol', newBalanceSymbol => { + if (this.balanceSymbol !== newBalanceSymbol) { + RouterController.goBack() + // Maybe reset state as well? + } + }), + NetworkController.subscribeKey('caipNetwork', newCaipNetwork => { + if (this.caipNetwork !== newCaipNetwork) { + this.caipNetwork = newCaipNetwork + } + }), + ConvertController.subscribe(newState => { + this.approvalTransaction = newState.approvalTransaction + this.convertTransaction = newState.convertTransaction + this.sourceToken = newState.sourceToken + this.gasPriceInUSD = newState.gasPriceInUSD + this.toToken = newState.toToken + this.transactionLoading = newState.transactionLoading + this.gasPriceInUSD = newState.gasPriceInUSD + this.transactionLoading = newState.transactionLoading + this.toTokenPriceInUSD = newState.toTokenPriceInUSD + this.sourceTokenAmount = newState.sourceTokenAmount ?? '' + this.toTokenAmount = newState.toTokenAmount ?? '' + this.priceImpact = newState.priceImpact + this.maxSlippage = newState.maxSlippage + }) + ] + ) + } + + // -- Render -------------------------------------------- // + public override render() { + return html` + ${this.templateSwap()} + ` + } + + // -- Private ------------------------------------------- // + private templateSwap() { + const sourceTokenText = `${formatNumberToLocalString(parseFloat(this.sourceTokenAmount))} ${this + .sourceToken?.symbol}` + const toTokenText = `${formatNumberToLocalString(parseFloat(this.toTokenAmount))} ${this.toToken + ?.symbol}` + + const sourceTokenValue = parseFloat(this.sourceTokenAmount) * this.sourceTokenPriceInUSD + const toTokenValue = + parseFloat(this.toTokenAmount) * this.toTokenPriceInUSD - (this.gasPriceInUSD || 0) + const sentPrice = formatNumberToLocalString(sourceTokenValue) + const receivePrice = formatNumberToLocalString(toTokenValue) + + return html` + + + + + Send + $${sentPrice} + + + + + + + + Receive + $${receivePrice} + + + + + + + ${this.templateDetails()} + + + + Review transaction carefully + + + + + + + + ` + } + + private templateDetails() { + const toTokenConvertedAmount = + this.sourceTokenPriceInUSD && this.toTokenPriceInUSD + ? (1 / this.toTokenPriceInUSD) * this.sourceTokenPriceInUSD + : 0 + + return html` + + ` + } + + private actionButtonLabel(): string { + if (this.approvalTransaction) { + return 'Approve' + } + + return 'Convert' + } + + private onCancelTransaction() { + RouterController.goBack() + } + + private onSendTransaction() { + if (this.approvalTransaction) { + ConvertController.sendTransactionForApproval(this.approvalTransaction) + } else { + ConvertController.sendTransactionForConvert(this.convertTransaction) + } + } +} + +declare global { + interface HTMLElementTagNameMap { + 'w3m-convert-preview-view': W3mConvertPreviewView + } +} diff --git a/packages/scaffold/src/views/w3m-convert-preview-view/styles.ts b/packages/scaffold/src/views/w3m-convert-preview-view/styles.ts new file mode 100644 index 0000000000..15ac06bb45 --- /dev/null +++ b/packages/scaffold/src/views/w3m-convert-preview-view/styles.ts @@ -0,0 +1,136 @@ +import { css } from 'lit' + +export default css` + :host > wui-flex:first-child { + overflow-y: auto; + overflow-x: hidden; + scrollbar-width: none; + } + + .preview-container, + .details-container { + width: 100%; + } + + .token-image { + width: 24px; + height: 24px; + box-shadow: 0 0 0 2px var(--wui-gray-glass-005); + border-radius: 12px; + } + + wui-loading-hexagon { + position: absolute; + } + + .token-item { + display: flex; + align-items: center; + justify-content: center; + gap: var(--wui-spacing-xxs); + padding: var(--wui-spacing-xs); + height: 40px; + border: none; + border-radius: 80px; + background: var(--wui-gray-glass-002); + box-shadow: inset 0 0 0 1px var(--wui-gray-glass-002); + cursor: pointer; + transition: background 0.2s linear; + } + + .token-item:hover { + background: var(--wui-gray-glass-005); + } + + .preview-token-details-container { + width: 100%; + } + + .details-row { + width: 100%; + padding: var(--wui-spacing-s) var(--wui-spacing-xl); + border-radius: var(--wui-border-radius-xxs); + background: var(--wui-gray-glass-002); + } + + .action-buttons-container { + width: 100%; + gap: var(--wui-spacing-xs); + } + + .action-buttons-container > button { + display: flex; + align-items: center; + justify-content: center; + background: transparent; + height: 48px; + border-radius: var(--wui-border-radius-xs); + border: none; + box-shadow: inset 0 0 0 1px var(--wui-gray-glass-010); + } + + .action-buttons-container > button:disabled { + opacity: 0.8; + cursor: not-allowed; + } + + .cancel-button:hover, + .convert-button:hover { + cursor: pointer; + } + + .action-buttons-container > button.cancel-button { + flex: 2; + } + + .action-buttons-container > button.convert-button { + flex: 4; + background-color: var(--wui-color-accent-090); + } + + .action-buttons-container > button.convert-button > wui-text { + color: white; + } + + .details-container > wui-flex { + background: var(--wui-gray-glass-002); + border-radius: var(--wui-border-radius-xxs); + width: 100%; + } + + .details-container > wui-flex > button { + border: none; + background: none; + padding: var(--wui-spacing-s); + border-radius: var(--wui-border-radius-xxs); + transition: background 0.2s linear; + } + + .details-container > wui-flex > button:hover { + background: var(--wui-gray-glass-002); + } + + .details-content-container { + padding: var(--wui-spacing-1xs); + display: flex; + align-items: center; + justify-content: center; + } + + .details-content-container > wui-flex { + width: 100%; + } + + .details-row { + width: 100%; + padding: var(--wui-spacing-s) var(--wui-spacing-xl); + border-radius: var(--wui-border-radius-xxs); + background: var(--wui-gray-glass-002); + } + + .free-badge { + background: rgba(38, 217, 98, 0.15); + border-radius: var(--wui-border-radius-4xs); + padding: 4.5px 6px; + } +` diff --git a/packages/scaffold/src/views/w3m-convert-select-token-view/index.ts b/packages/scaffold/src/views/w3m-convert-select-token-view/index.ts new file mode 100644 index 0000000000..af4d666ec9 --- /dev/null +++ b/packages/scaffold/src/views/w3m-convert-select-token-view/index.ts @@ -0,0 +1,249 @@ +import { customElement, interpolate } from '@web3modal/ui' +import { LitElement, html } from 'lit' +import styles from './styles.js' +import { RouterController, ConvertController } from '@web3modal/core' +import type { + TokenInfo, + TokenInfoWithBalance +} from '@web3modal/core/src/controllers/ConvertController.js' +import { state } from 'lit/decorators.js' + +@customElement('w3m-convert-select-token-view') +export class W3mConvertSelectTokenView extends LitElement { + public static override styles = styles + + private unsubscribe: ((() => void) | undefined)[] = [] + + // -- State & Properties -------------------------------- // + @state() private targetToken = RouterController.state.data?.target + + @state() private sourceToken = ConvertController.state.sourceToken + + @state() private toToken = ConvertController.state.toToken + + @state() private searchValue = '' + + // -- Lifecycle ----------------------------------------- // + public constructor() { + super() + + this.unsubscribe.push( + ...[ + ConvertController.subscribe(newState => { + this.sourceToken = newState.sourceToken + this.toToken = newState.toToken + }) + ] + ) + } + + private onSelectToken(token: TokenInfo) { + if (this.targetToken === 'sourceToken') { + ConvertController.setSourceToken(token) + } else { + ConvertController.setToToken(token) + } + RouterController.goBack() + } + + public override updated() { + const suggestedTokensContainer = this.renderRoot?.querySelector('.suggested-tokens-container') + suggestedTokensContainer?.addEventListener( + 'scroll', + this.handleSuggestedTokensScroll.bind(this) + ) + + const tokensList = this.renderRoot?.querySelector('.tokens') + tokensList?.addEventListener('scroll', this.handleTokenListScroll.bind(this)) + } + + public override disconnectedCallback() { + super.disconnectedCallback() + const suggestedTokensContainer = this.renderRoot?.querySelector('.suggested-tokens-container') + const tokensList = this.renderRoot?.querySelector('.tokens') + + suggestedTokensContainer?.removeEventListener( + 'scroll', + this.handleSuggestedTokensScroll.bind(this) + ) + tokensList?.removeEventListener('scroll', this.handleTokenListScroll.bind(this)) + } + + // -- Render -------------------------------------------- // + public override render() { + return html` + + ${this.templateSearchInput()} ${this.templateSuggestedTokens()} ${this.templateTokens()} + + ` + } + + // -- Private ------------------------------------------- // + private templateSearchInput() { + return html` + + + + ` + } + + private templateTokens() { + const yourTokens = ConvertController.state.myTokensWithBalance + ? Object.values(ConvertController.state.myTokensWithBalance) + : [] + const tokens = ConvertController.state.popularTokens + ? Object.values(ConvertController.state.popularTokens) + : [] + + const filteredYourTokens: TokenInfoWithBalance[] = this.filterTokensWithText< + TokenInfoWithBalance[] + >(yourTokens, this.searchValue) + const filteredTokens = this.filterTokensWithText(tokens, this.searchValue) + + return html` + + + ${filteredYourTokens?.length > 0 + ? html` + + Your tokens + + ${filteredYourTokens.map(tokenInfo => { + const selected = + tokenInfo.symbol === this.sourceToken?.symbol || + tokenInfo.symbol === this.toToken?.symbol + + return html` + { + if (!selected) { + this.onSelectToken(tokenInfo) + } + }} + > + + ` + })} + ` + : null} + + + Popular tokens + + + ${filteredTokens?.length > 0 + ? filteredTokens.map( + tokenInfo => html` + this.onSelectToken(tokenInfo)} + > + + ` + ) + : null} + + + ` + } + + private templateSuggestedTokens() { + const tokens = ConvertController.state.suggestedTokens + ? Object.values(ConvertController.state.suggestedTokens).slice(0, 8) + : null + + if (!tokens) { + return null + } + + return html` + + ${tokens.map( + tokenInfo => html` + this.onSelectToken(tokenInfo)} + > + + ` + )} + + ` + } + + private onSearchInputChange(event: CustomEvent) { + this.searchValue = event.detail + } + + private handleSuggestedTokensScroll() { + const container = this.renderRoot?.querySelector('.suggested-tokens-container') as + | HTMLElement + | undefined + + if (!container) { + return + } + + container.style.setProperty( + '--suggested-tokens-scroll--left-opacity', + interpolate([0, 100], [0, 1], container.scrollLeft).toString() + ) + container.style.setProperty( + '--suggested-tokens-scroll--right-opacity', + interpolate( + [0, 100], + [0, 1], + container.scrollWidth - container.scrollLeft - container.offsetWidth + ).toString() + ) + } + + private handleTokenListScroll() { + const container = this.renderRoot?.querySelector('.tokens') as HTMLElement | undefined + + if (!container) { + return + } + + container.style.setProperty( + '--tokens-scroll--top-opacity', + interpolate([0, 100], [0, 1], container.scrollTop).toString() + ) + container.style.setProperty( + '--tokens-scroll--bottom-opacity', + interpolate( + [0, 100], + [0, 1], + container.scrollHeight - container.scrollTop - container.offsetHeight + ).toString() + ) + } + + private filterTokensWithText(tokens: TokenInfo[], text: string) { + return tokens.filter(token => + `${token.symbol} ${token.name} ${token.address}`.toLowerCase().includes(text.toLowerCase()) + ) as T + } +} + +declare global { + interface HTMLElementTagNameMap { + 'w3m-convert-select-token-view': W3mConvertSelectTokenView + } +} diff --git a/packages/scaffold/src/views/w3m-convert-select-token-view/styles.ts b/packages/scaffold/src/views/w3m-convert-select-token-view/styles.ts new file mode 100644 index 0000000000..5b60af21ee --- /dev/null +++ b/packages/scaffold/src/views/w3m-convert-select-token-view/styles.ts @@ -0,0 +1,108 @@ +import { css } from 'lit' + +export default css` + :host { + --tokens-scroll--top-opacity: 0; + --tokens-scroll--bottom-opacity: 1; + --suggested-tokens-scroll--left-opacity: 0; + --suggested-tokens-scroll--right-opacity: 1; + } + + :host > wui-flex:first-child { + overflow-y: hidden; + overflow-x: hidden; + scrollbar-width: none; + scrollbar-height: none; + } + + wui-loading-hexagon { + position: absolute; + } + + .search-input-container, + .suggested-tokens-container { + padding-left: var(--wui-spacing-s); + padding-right: var(--wui-spacing-s); + } + + .tokens-container .tokens { + padding: 0px var(--wui-spacing-s); + padding-bottom: var(--wui-spacing-s); + } + + .search-input-container { + padding-top: var(--wui-spacing-s); + } + + .suggested-tokens-container { + overflow-x: auto; + mask-image: linear-gradient( + to right, + rgba(0, 0, 0, calc(1 - var(--suggested-tokens-scroll--left-opacity))) 0px, + rgba(200, 200, 200, calc(1 - var(--suggested-tokens-scroll--left-opacity))) 1px, + black 50px, + black 90px, + black calc(100% - 90px), + black calc(100% - 50px), + rgba(155, 155, 155, calc(1 - var(--suggested-tokens-scroll--right-opacity))) calc(100% - 1px), + rgba(0, 0, 0, calc(1 - var(--suggested-tokens-scroll--right-opacity))) 100% + ); + } + + .suggested-tokens-container::-webkit-scrollbar { + display: none; + } + + .tokens-container { + border-top: 1px solid var(--wui-gray-glass-005); + height: 100%; + max-height: 390px; + } + + .tokens { + width: 100%; + overflow-y: auto; + mask-image: linear-gradient( + to bottom, + rgba(0, 0, 0, calc(1 - var(--tokens-scroll--top-opacity))) 0px, + rgba(200, 200, 200, calc(1 - var(--tokens-scroll--top-opacity))) 1px, + black 50px, + black 90px, + black calc(100% - 90px), + black calc(100% - 50px), + rgba(155, 155, 155, calc(1 - var(--tokens-scroll--bottom-opacity))) calc(100% - 1px), + rgba(0, 0, 0, calc(1 - var(--tokens-scroll--bottom-opacity))) 100% + ); + } + + .network-search-input, + .select-network-button { + height: 40px; + } + + .select-network-button { + border: none; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: var(--wui-spacing-xs); + box-shadow: inset 0 0 0 1px var(--wui-gray-glass-005); + background-color: transparent; + border-radius: var(--wui-border-radius-xxs); + padding: var(--wui-spacing-xs); + align-items: center; + transition: background-color 0.2s linear; + } + + .select-network-button:hover { + background-color: var(--wui-gray-glass-002); + } + + .select-network-button > wui-image { + width: 26px; + height: 26px; + border-radius: var(--wui-border-radius-xs); + box-shadow: inset 0 0 0 1px var(--wui-gray-glass-010); + } +` diff --git a/packages/scaffold/src/views/w3m-convert-view/index.ts b/packages/scaffold/src/views/w3m-convert-view/index.ts new file mode 100644 index 0000000000..c2ea27a6cc --- /dev/null +++ b/packages/scaffold/src/views/w3m-convert-view/index.ts @@ -0,0 +1,323 @@ +import { customElement, formatNumberToLocalString } from '@web3modal/ui' +import { LitElement, html } from 'lit' +import { state } from 'lit/decorators.js' +import styles from './styles.js' +import { + ConvertController, + RouterController, + CoreHelperUtil, + NetworkController, + ModalController, + ConstantsUtil +} from '@web3modal/core' +import { NumberUtil } from '@web3modal/common' +import type { TokenInfo } from '@web3modal/core/src/utils/ConvertApiUtil.js' + +type Target = 'sourceToken' | 'toToken' + +@customElement('w3m-convert-view') +export class W3mConvertView extends LitElement { + public static override styles = styles + + private unsubscribe: ((() => void) | undefined)[] = [] + + // -- State & Properties -------------------------------- // + @state() private gasFeeIntervalId?: NodeJS.Timeout + + @state() private detailsOpen = false + + @state() private caipNetworkId = NetworkController.state.caipNetwork?.id + + @state() private initialized = ConvertController.state.initialized + + @state() private loading = ConvertController.state.loading + + @state() private loadingPrices = ConvertController.state.loadingPrices + + @state() private sourceToken = ConvertController.state.sourceToken + + @state() private sourceTokenAmount = ConvertController.state.sourceTokenAmount + + @state() private sourceTokenPriceInUSD = ConvertController.state.sourceTokenPriceInUSD + + @state() private toToken = ConvertController.state.toToken + + @state() private toTokenAmount = ConvertController.state.toTokenAmount + + @state() private toTokenPriceInUSD = ConvertController.state.toTokenPriceInUSD + + @state() private inputError = ConvertController.state.inputError + + @state() private gasPriceInUSD = ConvertController.state.gasPriceInUSD + + @state() private priceImpact = ConvertController.state.priceImpact + + @state() private maxSlippage = ConvertController.state.maxSlippage + + @state() private transactionLoading = ConvertController.state.transactionLoading + + // -- Lifecycle ----------------------------------------- // + public constructor() { + super() + + NetworkController.subscribeKey('caipNetwork', newCaipNetwork => { + if (this.caipNetworkId !== newCaipNetwork?.id) { + this.caipNetworkId = newCaipNetwork?.id + ConvertController.resetTokens() + ConvertController.resetValues() + ConvertController.initializeState() + } + }) + + this.unsubscribe.push( + ...[ + ModalController.subscribeKey('open', isOpen => { + if (!isOpen) { + ConvertController.resetValues() + } + }), + RouterController.subscribeKey('view', newRoute => { + if (!newRoute.includes('Convert')) { + ConvertController.resetValues() + } + }), + ConvertController.subscribeKey('sourceToken', newSourceToken => { + this.sourceToken = newSourceToken + }), + ConvertController.subscribeKey('toToken', newToToken => { + this.toToken = newToToken + }), + ConvertController.subscribe(newState => { + this.initialized = newState.initialized + this.loading = newState.loading + this.loadingPrices = newState.loadingPrices + this.transactionLoading = newState.transactionLoading + this.sourceToken = newState.sourceToken + this.sourceTokenAmount = newState.sourceTokenAmount + this.sourceTokenPriceInUSD = newState.sourceTokenPriceInUSD + this.toToken = newState.toToken + this.toTokenAmount = newState.toTokenAmount + this.toTokenPriceInUSD = newState.toTokenPriceInUSD + this.inputError = newState.inputError + this.gasPriceInUSD = newState.gasPriceInUSD + this.priceImpact = newState.priceImpact + this.maxSlippage = newState.maxSlippage + }) + ] + ) + + this.watchConvertValues() + } + + public override firstUpdated() { + if (!this.initialized) { + ConvertController.initializeState() + } + } + + public override disconnectedCallback() { + ConvertController.setLoading(false) + this.unsubscribe.forEach(unsubscribe => unsubscribe?.()) + clearInterval(this.gasFeeIntervalId) + } + + // -- Render -------------------------------------------- // + public override render() { + return html` + + ${this.initialized ? this.templateSwap() : this.templateLoading()} + + ` + } + + // -- Private ------------------------------------------- // + private watchConvertValues() { + this.gasFeeIntervalId = setInterval(() => ConvertController.refreshConvertValues(), 5000) + } + + private templateSwap() { + return html` + + + ${this.templateTokenInput('sourceToken', this.sourceToken)} + ${this.templateTokenInput('toToken', this.toToken)} ${this.templateReplaceTokensButton()} + + ${this.templateDetails()} ${this.templateActionButton()} + + ` + } + + private actionButtonLabel(): string { + if (this.inputError) { + return this.inputError + } + + return 'Review convert' + } + + private templateReplaceTokensButton() { + return html` + + ` + } + + private templateLoading() { + return html` + + + + ` + } + + private templateTokenInput(target: Target, token?: TokenInfo) { + const myToken = ConvertController.state.myTokensWithBalance?.[token?.address ?? ''] + const amount = target === 'toToken' ? this.toTokenAmount : this.sourceTokenAmount + const price = target === 'toToken' ? this.toTokenPriceInUSD : this.sourceTokenPriceInUSD + let value = parseFloat(amount) * price + + if (target === 'toToken') { + value -= this.gasPriceInUSD || 0 + } + + return html`` + } + + private onSetMaxValue(target: Target, balance: string | undefined) { + const token = target === 'sourceToken' ? this.sourceToken : this.toToken + const isNetworkToken = token?.address === ConstantsUtil.NATIVE_TOKEN_ADDRESS + + let value = '0' + + if (!balance) { + value = '0' + this.handleChangeAmount(target, value) + + return + } + + if (!this.gasPriceInUSD) { + value = balance + this.handleChangeAmount(target, value) + + return + } + + const amountOfTokenGasRequires = NumberUtil.bigNumber(this.gasPriceInUSD.toFixed(5)).dividedBy( + this.sourceTokenPriceInUSD + ) + const maxValue = isNetworkToken + ? NumberUtil.bigNumber(balance).minus(amountOfTokenGasRequires) + : NumberUtil.bigNumber(balance) + + this.handleChangeAmount(target, maxValue.isGreaterThan(0) ? maxValue.toFixed(20) : '0') + } + + private templateDetails() { + if (this.loading || this.inputError) { + return null + } + + if (!this.sourceToken || !this.toToken || !this.sourceTokenAmount || !this.toTokenAmount) { + return null + } + + const toTokenConvertedAmount = + this.sourceTokenPriceInUSD && this.toTokenPriceInUSD + ? (1 / this.toTokenPriceInUSD) * this.sourceTokenPriceInUSD + : 0 + + return html` + + ` + } + + private handleChangeAmount(target: Target, value: string) { + ConvertController.clearError() + if (target === 'sourceToken') { + ConvertController.setSourceTokenAmount(value) + } else { + ConvertController.setToTokenAmount(value) + } + this.onDebouncedGetSwapCalldata() + } + + private templateActionButton() { + const haveNoTokenSelected = !this.toToken || !this.sourceToken + const loading = this.loading || this.loadingPrices || this.transactionLoading + + return html` + + ${this.actionButtonLabel()} + + ` + } + + private onDebouncedGetSwapCalldata = CoreHelperUtil.debounce(async () => { + await ConvertController.convertTokens() + }, 500) + + private onSwitchTokens() { + ConvertController.switchTokens() + } + + private onConvertPreview() { + RouterController.push('ConvertPreview') + } +} + +declare global { + interface HTMLElementTagNameMap { + 'w3m-convert-view': W3mConvertView + } +} diff --git a/packages/scaffold/src/views/w3m-convert-view/styles.ts b/packages/scaffold/src/views/w3m-convert-view/styles.ts new file mode 100644 index 0000000000..408f50a270 --- /dev/null +++ b/packages/scaffold/src/views/w3m-convert-view/styles.ts @@ -0,0 +1,93 @@ +import { css } from 'lit' + +export default css` + :host > wui-flex:first-child { + overflow-y: auto; + overflow-x: hidden; + scrollbar-width: none; + } + + wui-loading-hexagon { + position: absolute; + } + + .action-button { + width: 100%; + border-radius: var(--wui-border-radius-xs); + } + + .action-button:disabled { + border-color: 1px solid var(--wui-gray-glass-005); + } + + .convert-inputs-container { + position: relative; + } + + .replace-tokens-button { + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + gap: var(--wui-spacing-1xs); + height: 40px; + width: 40px; + padding: var(--wui-spacing-xs); + border: none; + border-radius: var(--wui-border-radius-xxs); + background: var(--wui-gray-glass-005); + transition: background-color var(--wui-duration-md) var(--wui-ease-out-power-1); + will-change: background-color; + z-index: 20; + } + + .replace-tokens-button:hover { + background: var(--wui-gray-glass-010); + } + + .details-container > wui-flex { + background: var(--wui-gray-glass-002); + border-radius: var(--wui-border-radius-xxs); + width: 100%; + } + + .details-container > wui-flex > button { + border: none; + background: none; + padding: var(--wui-spacing-s); + border-radius: var(--wui-border-radius-xxs); + transition: background 0.2s linear; + } + + .details-container > wui-flex > button:hover { + background: var(--wui-gray-glass-002); + } + + .details-content-container { + padding: var(--wui-spacing-1xs); + display: flex; + align-items: center; + justify-content: center; + } + + .details-content-container > wui-flex { + width: 100%; + } + + .details-row { + width: 100%; + padding: var(--wui-spacing-s) var(--wui-spacing-xl); + border-radius: var(--wui-border-radius-xxs); + background: var(--wui-gray-glass-002); + } + + .free-badge { + background: rgba(38, 217, 98, 0.15); + border-radius: var(--wui-border-radius-4xs); + padding: 4.5px 6px; + } +` diff --git a/packages/solana/src/client.ts b/packages/solana/src/client.ts index 327ab91223..35dbfe94b4 100644 --- a/packages/solana/src/client.ts +++ b/packages/solana/src/client.ts @@ -140,7 +140,19 @@ export class Web3Modal extends Web3ModalScaffold { }) return signature as string - } + }, + + sendTransaction: async () => { + return await Promise.resolve('0x') + }, + + getEstimatedGas: async () => { + return await Promise.resolve(BigInt(0)) + }, + + parseUnits: () => BigInt(0), + + formatUnits: () => '' } super({ diff --git a/packages/ui/index.ts b/packages/ui/index.ts index 0998566d43..023ab9edf8 100644 --- a/packages/ui/index.ts +++ b/packages/ui/index.ts @@ -16,7 +16,10 @@ export * from './src/composites/wui-card-select-loader/index.js' export * from './src/composites/wui-card-select/index.js' export * from './src/composites/wui-chip/index.js' export * from './src/composites/wui-connect-button/index.js' +export * from './src/composites/wui-convert-details/index.js' export * from './src/composites/wui-cta-button/index.js' +export * from './src/composites/wui-details-group/index.js' +export * from './src/composites/wui-details-group-item/index.js' export * from './src/composites/wui-email-input/index.js' export * from './src/composites/wui-icon-box/index.js' export * from './src/composites/wui-icon-link/index.js' @@ -36,9 +39,12 @@ export * from './src/composites/wui-otp/index.js' export * from './src/composites/wui-qr-code/index.js' export * from './src/composites/wui-search-bar/index.js' export * from './src/composites/wui-snackbar/index.js' +export * from './src/composites/wui-convert-input/index.js' export * from './src/composites/wui-tabs/index.js' +export * from './src/composites/wui-token-button/index.js' export * from './src/composites/wui-tag/index.js' export * from './src/composites/wui-tooltip/index.js' +export * from './src/composites/wui-token-list-item/index.js' export * from './src/composites/wui-transaction-visual/index.js' export * from './src/composites/wui-visual-thumbnail/index.js' export * from './src/composites/wui-wallet-image/index.js' @@ -66,6 +72,8 @@ export * from './src/layout/wui-flex/index.js' export * from './src/layout/wui-grid/index.js' export * from './src/layout/wui-separator/index.js' +export * from './src/utils/Math.js' +export * from './src/utils/NumberUtil.js' export { initializeTheming, setColorTheme, setThemeVariables } from './src/utils/ThemeUtil.js' export { UiHelperUtil } from './src/utils/UiHelperUtil.js' export { TransactionUtil } from './src/utils/TransactionUtil.js' diff --git a/packages/ui/src/assets/svg/checkmark-bold.ts b/packages/ui/src/assets/svg/checkmark-bold.ts new file mode 100644 index 0000000000..b7f80f79d4 --- /dev/null +++ b/packages/ui/src/assets/svg/checkmark-bold.ts @@ -0,0 +1,10 @@ +import { svg } from 'lit' + +export const checkmarkBoldSvg = svg` + +` diff --git a/packages/ui/src/assets/svg/swapHorizontalRoundedBold.ts b/packages/ui/src/assets/svg/swapHorizontalRoundedBold.ts new file mode 100644 index 0000000000..302e1ade80 --- /dev/null +++ b/packages/ui/src/assets/svg/swapHorizontalRoundedBold.ts @@ -0,0 +1,5 @@ +import { svg } from 'lit' + +export const swapHorizontalRoundedBoldSvg = svg` + +` diff --git a/packages/ui/src/components/wui-icon/index.ts b/packages/ui/src/components/wui-icon/index.ts index f168dc62db..4b1e4d8e6e 100644 --- a/packages/ui/src/components/wui-icon/index.ts +++ b/packages/ui/src/components/wui-icon/index.ts @@ -54,6 +54,7 @@ import { sendSvg } from '../../assets/svg/send.js' import { swapHorizontalSvg } from '../../assets/svg/swapHorizontal.js' import { swapHorizontalBoldSvg } from '../../assets/svg/swapHorizontalBold.js' import { swapHorizontalMediumSvg } from '../../assets/svg/swapHorizontalMedium.js' +import { swapHorizontalRoundedBoldSvg } from '../../assets/svg/swapHorizontalRoundedBold.js' import { swapVerticalSvg } from '../../assets/svg/swapVertical.js' import { telegramSvg } from '../../assets/svg/telegram.js' import { twitchSvg } from '../../assets/svg/twitch.js' @@ -70,6 +71,7 @@ import { bankSvg } from '../../assets/svg/bank.js' import { cardSvg } from '../../assets/svg/card.js' import { plusSvg } from '../../assets/svg/plus.js' import { cursorTransparentSvg } from '../../assets/svg/cursor-transparent.js' +import { checkmarkBoldSvg } from '../../assets/svg/checkmark-bold.js' const svgOptions: Record> = { add: addSvg, @@ -85,6 +87,7 @@ const svgOptions: Record> = { browser: browserSvg, card: cardSvg, checkmark: checkmarkSvg, + checkmarkBold: checkmarkBoldSvg, chevronBottom: chevronBottomSvg, chevronLeft: chevronLeftSvg, chevronRight: chevronRightSvg, @@ -124,6 +127,7 @@ const svgOptions: Record> = { swapHorizontal: swapHorizontalSvg, swapHorizontalMedium: swapHorizontalMediumSvg, swapHorizontalBold: swapHorizontalBoldSvg, + swapHorizontalRoundedBold: swapHorizontalRoundedBoldSvg, swapVertical: swapVerticalSvg, telegram: telegramSvg, twitch: twitchSvg, diff --git a/packages/ui/src/components/wui-image/index.ts b/packages/ui/src/components/wui-image/index.ts index 4593355441..4237e81ec1 100644 --- a/packages/ui/src/components/wui-image/index.ts +++ b/packages/ui/src/components/wui-image/index.ts @@ -3,6 +3,7 @@ import { property } from 'lit/decorators.js' import { colorStyles, resetStyles } from '../../utils/ThemeUtil.js' import { customElement } from '../../utils/WebComponentsUtil.js' import styles from './styles.js' +import type { SizeType } from '../../utils/TypeUtil.js' @customElement('wui-image') export class WuiImage extends LitElement { @@ -13,8 +14,15 @@ export class WuiImage extends LitElement { @property() public alt = 'Image' + @property() public size?: SizeType = undefined + // -- Render -------------------------------------------- // public override render() { + this.style.cssText = ` + --local-width: ${this.size ? `var(--wui-icon-size-${this.size});` : '100%'}; + --local-height: ${this.size ? `var(--wui-icon-size-${this.size});` : '100%'}; + ` + return html`${this.alt}` } } diff --git a/packages/ui/src/components/wui-image/styles.ts b/packages/ui/src/components/wui-image/styles.ts index de81aad1f3..3aa19a01ad 100644 --- a/packages/ui/src/components/wui-image/styles.ts +++ b/packages/ui/src/components/wui-image/styles.ts @@ -3,8 +3,8 @@ import { css } from 'lit' export default css` :host { display: block; - width: 100%; - height: 100%; + width: var(--local-width); + height: var(--local-height); } img { diff --git a/packages/ui/src/composites/wui-button/styles.ts b/packages/ui/src/composites/wui-button/styles.ts index 589603c9e9..a4e62d9dc2 100644 --- a/packages/ui/src/composites/wui-button/styles.ts +++ b/packages/ui/src/composites/wui-button/styles.ts @@ -10,6 +10,7 @@ export default css` border: 1px solid var(--wui-gray-glass-010); border-radius: var(--local-border-radius); width: var(--local-width); + white-space: nowrap; } button:disabled { diff --git a/packages/ui/src/composites/wui-convert-details/index.ts b/packages/ui/src/composites/wui-convert-details/index.ts new file mode 100644 index 0000000000..7de2390202 --- /dev/null +++ b/packages/ui/src/composites/wui-convert-details/index.ts @@ -0,0 +1,129 @@ +import { html, LitElement } from 'lit' +import { property } from 'lit/decorators.js' +import { customElement } from '../../utils/WebComponentsUtil.js' +import { resetStyles } from '../../utils/ThemeUtil.js' +import styles from './styles.js' +import { formatNumberToLocalString } from '../../utils/NumberUtil.js' + +@customElement('wui-convert-details') +export class WuiConvertDetails extends LitElement { + public static override styles = [resetStyles, styles] + + // -- State & Properties -------------------------------- // + @property() public detailsOpen = false + + @property() public sourceTokenSymbol?: number + + @property() public sourceTokenPrice?: number + + @property() public toTokenSymbol?: number + + @property() public toTokenConvertedAmount?: number + + @property() public gasPriceInUSD?: number + + @property() public priceImpact?: number + + @property() public slippageRate = 0.5 + + @property() public maxSlippage?: number + + // -- Render -------------------------------------------- // + public override render() { + return html` + + + + ${this.detailsOpen + ? html` + + + + Network cost + + $${formatNumberToLocalString(this.gasPriceInUSD, 3)} + + + + ${this.priceImpact + ? html` + + Price impact + + + ${formatNumberToLocalString(this.priceImpact, 3)}% + + + + ` + : null} + ${this.maxSlippage && this.sourceTokenSymbol + ? html` + + Max. slippage + + + ${formatNumberToLocalString(this.maxSlippage, 6)} + ${this.sourceTokenSymbol} ${this.slippageRate}% + + + + ` + : null} + + + Provider fee + + Free + + + + + ` + : null} + + + ` + } + + // -- Private ------------------------------------------- // + private toggleDetails() { + this.detailsOpen = !this.detailsOpen + } +} + +declare global { + interface HTMLElementTagNameMap { + 'wui-convert-details': WuiConvertDetails + } +} diff --git a/packages/ui/src/composites/wui-convert-details/styles.ts b/packages/ui/src/composites/wui-convert-details/styles.ts new file mode 100644 index 0000000000..b6abae5746 --- /dev/null +++ b/packages/ui/src/composites/wui-convert-details/styles.ts @@ -0,0 +1,52 @@ +import { css } from 'lit' + +export default css` + :host { + width: 100%; + } + + .details-container > wui-flex { + background: var(--wui-gray-glass-002); + border-radius: var(--wui-border-radius-xxs); + width: 100%; + } + + .details-container > wui-flex > button { + border: none; + background: none; + padding: var(--wui-spacing-s); + border-radius: var(--wui-border-radius-xxs); + cursor: pointer; + } + + .details-content-container { + padding: var(--wui-spacing-1xs); + padding-top: 0px; + display: flex; + align-items: center; + justify-content: center; + } + + .details-content-container > wui-flex { + width: 100%; + } + + .details-row { + width: 100%; + padding: var(--wui-spacing-s); + padding-left: var(--wui-spacing-s); + padding-right: var(--wui-spacing-1xs); + border-radius: calc(var(--wui-border-radius-5xs) + var(--wui-border-radius-4xs)); + background: var(--wui-gray-glass-002); + } + + .details-row.provider-free-row { + padding-right: var(--wui-spacing-xs); + } + + .free-badge { + background: rgba(38, 217, 98, 0.15); + border-radius: var(--wui-border-radius-4xs); + padding: 4.5px 6px; + } +` diff --git a/packages/ui/src/composites/wui-convert-input/index.ts b/packages/ui/src/composites/wui-convert-input/index.ts new file mode 100644 index 0000000000..1fbf80edab --- /dev/null +++ b/packages/ui/src/composites/wui-convert-input/index.ts @@ -0,0 +1,282 @@ +import { html, LitElement } from 'lit' +import { property } from 'lit/decorators.js' +import { customElement } from '../../utils/WebComponentsUtil.js' +import { resetStyles } from '../../utils/ThemeUtil.js' +import '../../components/wui-text/index.js' +import '../wui-transaction-visual/index.js' +import { EventsController, RouterController } from '@web3modal/core' +import styles from './styles.js' +import { formatNumberToLocalString } from '../../utils/NumberUtil.js' +import { NumberUtil } from '@web3modal/common' + +const MINIMUM_USD_VALUE_TO_CONVERT = 0.00005 + +type Target = 'sourceToken' | 'toToken' + +interface TokenInfo { + address: `0x${string}` + symbol: string + name: string + decimals: number + logoURI: string + domainVersion?: string + eip2612?: boolean + isFoT?: boolean + tags?: string[] +} + +@customElement('wui-convert-input') +export class WuiConvertInput extends LitElement { + public static override styles = [resetStyles, styles] + + // -- State & Properties -------------------------------- // + @property() public focused = false + + @property() public balance: string | undefined + + @property() public value?: string + + @property() public price = 0 + + @property() public marketValue?: string = '$1.0345,00' + + @property() public disabled?: boolean + + @property() public target: Target = 'sourceToken' + + @property() public token?: TokenInfo + + @property() public onSetAmount: ((target: Target, value: string) => void) | null = null + + @property() public onSetMaxValue: ((target: Target, balance: string | undefined) => void) | null = + null + + // -- Render -------------------------------------------- // + public override render() { + const marketValue = this.marketValue || '0' + const isMarketValueGreaterThanZero = NumberUtil.bigNumber(marketValue).isGreaterThan(0) + + return html` + + ${this.target === 'sourceToken' + ? this.templateSourceInputBackgroundMask() + : this.templateToInputBackgroundMask()} + + this.onFocusChange(true)} + @focusout=${() => this.onFocusChange(false)} + ?disabled=${this.disabled} + .value=${this.value} + @input=${this.dispatchInputChangeEvent} + @keydown=${this.handleKeydown} + placeholder="0" + /> + + ${isMarketValueGreaterThanZero ? `$${this.marketValue}` : null} + + + ${this.templateTokenSelectButton()} + + ` + } + + // -- Private ------------------------------------------- // + private handleKeydown(event: KeyboardEvent) { + const allowedKeys = [ + 'Backspace', + 'Meta', + 'Ctrl', + 'a', + 'c', + 'v', + 'ArrowLeft', + 'ArrowRight', + 'Tab' + ] + const isComma = event.key === ',' + const isDot = event.key === '.' + const isNumericKey = event.key >= '0' && event.key <= '9' + const currentValue = this.value + + if (!isNumericKey && !allowedKeys.includes(event.key) && !isDot && !isComma) { + event.preventDefault() + } + + if (isComma || isDot) { + if (currentValue?.includes('.') || currentValue?.includes(',')) { + event.preventDefault() + } + } + } + + private dispatchInputChangeEvent(event: InputEvent) { + if (!this.onSetAmount) { + return + } + + const value = (event.target as HTMLInputElement).value + if (value === ',' || value === '.') { + this.onSetAmount(this.target, '0.') + } else if (value.endsWith(',')) { + this.onSetAmount(this.target, value.replace(',', '.')) + } else { + this.onSetAmount(this.target, value) + } + } + + private setMaxValueToInput() { + this.onSetMaxValue?.(this.target, this.balance) + } + + private templateTokenSelectButton() { + if (!this.token) { + return html` + Select token + ` + } + + const tokenElement = this.token.logoURI + ? html`` + : html` + + ` + + return html` + + + ${this.tokenBalanceTemplate()} + + ` + } + + private tokenBalanceTemplate() { + const balanceValueInUSD = NumberUtil.multiply(this.balance, this.price) + const haveBalance = balanceValueInUSD + ? balanceValueInUSD?.isGreaterThan(MINIMUM_USD_VALUE_TO_CONVERT) + : false + + return html` + ${haveBalance + ? html` + ${formatNumberToLocalString(this.balance, 3)} + ` + : null} + ${this.target === 'sourceToken' ? this.tokenActionButtonTemplate(haveBalance) : null} + ` + } + + private templateSourceInputBackgroundMask() { + return html` + + + + + + + + ` + } + + private templateToInputBackgroundMask() { + return html` + + + + + + + + ` + } + + private tokenActionButtonTemplate(_haveBalance: boolean) { + if (_haveBalance) { + return html` ` + } + + return html` ` + } + + private onFocusChange(state: boolean) { + this.focused = state + } + + private onSelectToken() { + EventsController.sendEvent({ type: 'track', event: 'CLICK_SELECT_TOKEN_TO_SWAP' }) + RouterController.push('ConvertSelectToken', { + target: this.target + }) + } + + private onBuyToken() { + RouterController.push('OnRampProviders') + } +} + +declare global { + interface HTMLElementTagNameMap { + 'wui-convert-input': WuiConvertInput + } +} diff --git a/packages/ui/src/composites/wui-convert-input/styles.ts b/packages/ui/src/composites/wui-convert-input/styles.ts new file mode 100644 index 0000000000..a9773efbb0 --- /dev/null +++ b/packages/ui/src/composites/wui-convert-input/styles.ts @@ -0,0 +1,129 @@ +import { css } from 'lit' + +export default css` + :host > wui-flex { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + border-radius: var(--wui-border-radius-s); + padding: var(--wui-spacing-xl); + padding-right: var(--wui-spacing-s); + width: 100%; + height: 100px; + box-sizing: border-box; + position: relative; + } + + :host > wui-flex > svg.input_mask { + position: absolute; + inset: 0; + z-index: 5; + } + + :host wui-flex .input_mask__border, + :host wui-flex .input_mask__background { + transition: fill var(--wui-duration-md) var(--wui-ease-out-power-1); + will-change: fill; + } + + :host wui-flex .input_mask__border { + fill: var(--wui-gray-glass-005); + } + + :host wui-flex .input_mask__background { + fill: var(--wui-gray-glass-002); + } + + :host wui-flex.focus .input_mask__border { + fill: var(--wui-gray-glass-020); + } + + :host > wui-flex .swap-input, + :host > wui-flex .swap-token-button { + z-index: 10; + } + + :host > wui-flex .swap-input { + -webkit-mask-image: linear-gradient( + 270deg, + transparent 0px, + transparent 8px, + black 24px, + black 25px, + black 32px, + black 100% + ); + mask-image: linear-gradient( + 270deg, + transparent 0px, + transparent 8px, + black 24px, + black 25px, + black 32px, + black 100% + ); + } + + :host > wui-flex .swap-input input { + background: none; + border: none; + height: 42px; + width: 100%; + font-size: 32px; + font-style: normal; + font-weight: 400; + line-height: 130%; + letter-spacing: -1.28px; + outline: none; + caret-color: var(--wui-color-accent-100); + } + + :host > wui-flex .swap-input input:focus-visible { + outline: none; + } + + :host > wui-flex .swap-input input::-webkit-outer-spin-button, + :host > wui-flex .swap-input input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + + .token-select-button { + display: flex; + align-items: center; + justify-content: center; + gap: var(--wui-spacing-xxs); + padding: var(--wui-spacing-xs); + padding-right: var(--wui-spacing-1xs); + height: 40px; + border: none; + border-radius: 80px; + background: var(--wui-gray-glass-002); + box-shadow: inset 0 0 0 1px var(--wui-gray-glass-002); + cursor: pointer; + transition: background 0.2s linear; + } + + .token-select-button:hover { + background: var(--wui-gray-glass-005); + } + + .token-select-button wui-image { + width: 24px; + height: 24px; + border-radius: var(--wui-border-radius-s); + box-shadow: inset 0 0 0 1px var(--wui-gray-glass-010); + } + + .max-value-button { + background-color: transparent; + border: none; + cursor: pointer; + color: var(--wui-gray-glass-020); + } + + .market-value { + min-height: 18px; + } +` diff --git a/packages/ui/src/composites/wui-details-group-item/index.ts b/packages/ui/src/composites/wui-details-group-item/index.ts new file mode 100644 index 0000000000..059c037590 --- /dev/null +++ b/packages/ui/src/composites/wui-details-group-item/index.ts @@ -0,0 +1,32 @@ +import { html, LitElement } from 'lit' +import { property } from 'lit/decorators.js' +import '../../layout/wui-flex/index.js' +import { elementStyles, resetStyles } from '../../utils/ThemeUtil.js' +import { customElement } from '../../utils/WebComponentsUtil.js' +import styles from './styles.js' + +@customElement('wui-details-group-item') +export class WuiDetailsGroupItem extends LitElement { + public static override styles = [resetStyles, elementStyles, styles] + + // -- State & Properties -------------------------------- // + @property() public name = '' + + // -- Render -------------------------------------------- // + public override render() { + return html` + + ${this.name} + + + + + ` + } +} + +declare global { + interface HTMLElementTagNameMap { + 'wui-details-group-item': WuiDetailsGroupItem + } +} diff --git a/packages/ui/src/composites/wui-details-group-item/styles.ts b/packages/ui/src/composites/wui-details-group-item/styles.ts new file mode 100644 index 0000000000..188c5c0ccd --- /dev/null +++ b/packages/ui/src/composites/wui-details-group-item/styles.ts @@ -0,0 +1,11 @@ +import { css } from 'lit' + +export default css` + :host { + display: flex; + flex-direction: row; + gap: var(--wui-spacing-l); + width: 100%; + border-radius: var(--wui-border-radius-xs); + } +` diff --git a/packages/ui/src/composites/wui-details-group/index.ts b/packages/ui/src/composites/wui-details-group/index.ts new file mode 100644 index 0000000000..480934391e --- /dev/null +++ b/packages/ui/src/composites/wui-details-group/index.ts @@ -0,0 +1,25 @@ +import { html, LitElement } from 'lit' +import '../../layout/wui-flex/index.js' +import { elementStyles, resetStyles } from '../../utils/ThemeUtil.js' +import { customElement } from '../../utils/WebComponentsUtil.js' +import styles from './styles.js' + +@customElement('wui-details-group') +export class WuiDetailsGroup extends LitElement { + public static override styles = [resetStyles, elementStyles, styles] + + // -- Render -------------------------------------------- // + public override render() { + return html` + + + + ` + } +} + +declare global { + interface HTMLElementTagNameMap { + 'wui-details-group': WuiDetailsGroup + } +} diff --git a/packages/ui/src/composites/wui-details-group/styles.ts b/packages/ui/src/composites/wui-details-group/styles.ts new file mode 100644 index 0000000000..2bd7881ffa --- /dev/null +++ b/packages/ui/src/composites/wui-details-group/styles.ts @@ -0,0 +1,11 @@ +import { css } from 'lit' + +export default css` + :host { + display: block; + padding: var(--wui-spacing-l) var(--wui-spacing-m); + background-color: var(--wui-gray-glass-002); + border-radius: var(--wui-border-radius-xs); + width: 100%; + } +` diff --git a/packages/ui/src/composites/wui-input-text/index.ts b/packages/ui/src/composites/wui-input-text/index.ts index 65c14cce8f..e72fbfe2dd 100644 --- a/packages/ui/src/composites/wui-input-text/index.ts +++ b/packages/ui/src/composites/wui-input-text/index.ts @@ -28,7 +28,7 @@ export class WuiInputText extends LitElement { @property() public keyHint?: HTMLInputElement['enterKeyHint'] - @property() public value?: string + @property() public value?: string = '' // -- Render -------------------------------------------- // public override render() { @@ -43,7 +43,6 @@ export class WuiInputText extends LitElement { ?disabled=${this.disabled} placeholder=${this.placeholder} @input=${this.dispatchInputChangeEvent.bind(this)} - value=${ifDefined(this.value)} .value=${this.value || ''} /> ` diff --git a/packages/ui/src/composites/wui-list-token/index.ts b/packages/ui/src/composites/wui-list-token/index.ts index fe1101a914..74e2c4027f 100644 --- a/packages/ui/src/composites/wui-list-token/index.ts +++ b/packages/ui/src/composites/wui-list-token/index.ts @@ -33,11 +33,10 @@ export class WuiListToken extends LitElement { ${this.tokenName} - ${UiHelperUtil.roundNumber(Number(this.tokenAmount), 6, 5)} - ${this.tokenCurrency} + + ${UiHelperUtil.formatNumberToLocalString(this.tokenAmount, 4)} ${this.tokenCurrency} + + $${this.tokenValue.toFixed(2)} diff --git a/packages/ui/src/composites/wui-token-button/styles.ts b/packages/ui/src/composites/wui-token-button/styles.ts index 0bb57acb6f..cb252eff47 100644 --- a/packages/ui/src/composites/wui-token-button/styles.ts +++ b/packages/ui/src/composites/wui-token-button/styles.ts @@ -12,6 +12,7 @@ export default css` height: 40px; border-radius: var(--wui-border-radius-3xl); background: var(--wui-gray-glass-002); + border-width: 0px; box-shadow: inset 0 0 0 1px var(--wui-gray-glass-002); } diff --git a/packages/ui/src/composites/wui-token-list-item/index.ts b/packages/ui/src/composites/wui-token-list-item/index.ts new file mode 100644 index 0000000000..5e8f4d67df --- /dev/null +++ b/packages/ui/src/composites/wui-token-list-item/index.ts @@ -0,0 +1,74 @@ +import { html, LitElement } from 'lit' +import { property } from 'lit/decorators.js' +import '../../components/wui-icon/index.js' +import '../../components/wui-image/index.js' +import '../../components/wui-loading-spinner/index.js' +import '../../components/wui-text/index.js' +import '../../layout/wui-flex/index.js' +import { elementStyles, resetStyles } from '../../utils/ThemeUtil.js' +import { customElement } from '../../utils/WebComponentsUtil.js' +import '../wui-icon-box/index.js' +import styles from './styles.js' +import { formatNumberToLocalString } from '../../utils/NumberUtil.js' +import { NumberUtil } from '@web3modal/common' + +@customElement('wui-token-list-item') +export class WuiTokenListItem extends LitElement { + public static override styles = [resetStyles, elementStyles, styles] + + // -- State & Properties -------------------------------- // + @property() public imageSrc?: string = undefined + + @property() public name?: string = undefined + + @property() public symbol?: string = undefined + + @property() public price?: string = undefined + + @property() public amount?: string = undefined + + // -- Render -------------------------------------------- // + public override render() { + const value = NumberUtil.multiply(this.price, this.amount)?.toFixed(3) + + return html` + + ${this.visualTemplate()} + + + ${this.name} + ${value + ? html` + + $${formatNumberToLocalString(value, 3)} + + ` + : null} + + + ${this.symbol} + ${this.amount && + html`${formatNumberToLocalString(this.amount, 4)}`} + + + + ` + } + + // -- Private ------------------------------------------- // + private visualTemplate() { + if (this.imageSrc) { + return html`` + } + + return null + } +} + +declare global { + interface HTMLElementTagNameMap { + 'wui-token-list-item': WuiTokenListItem + } +} diff --git a/packages/ui/src/composites/wui-token-list-item/styles.ts b/packages/ui/src/composites/wui-token-list-item/styles.ts new file mode 100644 index 0000000000..6b5aa1f3a2 --- /dev/null +++ b/packages/ui/src/composites/wui-token-list-item/styles.ts @@ -0,0 +1,55 @@ +import { css } from 'lit' + +export default css` + :host > wui-flex { + cursor: pointer; + display: flex; + column-gap: var(--wui-spacing-s); + padding: var(--wui-spacing-xs); + padding-right: var(--wui-spacing-l); + width: 100%; + background-color: transparent; + border-radius: var(--wui-border-radius-xs); + color: var(--wui-color-fg-250); + transition: background-color 0.2s linear; + } + + :host > wui-flex:hover { + background-color: var(--wui-gray-glass-002); + } + + :host([disabled]) > wui-flex { + opacity: 0.6; + } + + :host([disabled]) > wui-flex:hover { + background-color: transparent; + } + + :host > wui-flex > wui-flex { + flex: 1; + } + + :host > wui-flex > wui-image { + width: 40px; + height: 40px; + border-radius: var(--wui-border-radius-3xl); + position: relative; + } + + :host > wui-flex > wui-image::after { + position: absolute; + content: ''; + inset: 0; + box-shadow: inset 0 0 0 1px var(--wui-gray-glass-010); + border-radius: var(--wui-border-radius-l); + } + + button > wui-icon-box[data-variant='square-blue'] { + border-radius: var(--wui-border-radius-3xs); + position: relative; + border: none; + width: 36px; + height: 36px; + } +` diff --git a/packages/ui/src/utils/JSXTypeUtil.ts b/packages/ui/src/utils/JSXTypeUtil.ts index 20f8215b7b..c23b320b1f 100644 --- a/packages/ui/src/utils/JSXTypeUtil.ts +++ b/packages/ui/src/utils/JSXTypeUtil.ts @@ -11,53 +11,61 @@ import type { WuiVisual } from '../components/wui-visual/index.js' import type { WuiAccountButton } from '../composites/wui-account-button/index.js' import type { WuiAllWalletsImage } from '../composites/wui-all-wallets-image/index.js' import type { WuiAvatar } from '../composites/wui-avatar/index.js' +import type { WuiBalance } from '../composites/wui-balance/index.js' +import type { WuiBanner } from '../composites/wui-banner/index.js' import type { WuiButton } from '../composites/wui-button/index.js' -import type { WuiCardSelectLoader } from '../composites/wui-card-select-loader/index.js' import type { WuiCardSelect } from '../composites/wui-card-select/index.js' +import type { WuiCardSelectLoader } from '../composites/wui-card-select-loader/index.js' import type { WuiChip } from '../composites/wui-chip/index.js' +import type { WuiCompatibleNetwork } from '../composites/wui-compatible-network/index.js' +import type { WuiChipButton } from '../composites/wui-chip-button/index.js' import type { WuiConnectButton } from '../composites/wui-connect-button/index.js' +import type { WuiConvertDetails } from '../composites/wui-convert-details/index.js' +import type { WuiConvertInput } from '../composites/wui-convert-input/index.js' import type { WuiCtaButton } from '../composites/wui-cta-button/index.js' +import type { WuiDetailsGroup } from '../composites/wui-details-group/index.js' +import type { WuiDetailsGroupItem } from '../composites/wui-details-group-item/index.js' import type { WuiEmailInput } from '../composites/wui-email-input/index.js' import type { WuiIconBox } from '../composites/wui-icon-box/index.js' import type { WuiIconLink } from '../composites/wui-icon-link/index.js' +import type { WuiInputAmount } from '../composites/wui-input-amount/index.js' import type { WuiInputElement } from '../composites/wui-input-element/index.js' import type { WuiInputNumeric } from '../composites/wui-input-numeric/index.js' import type { WuiInputText } from '../composites/wui-input-text/index.js' import type { WuiLink } from '../composites/wui-link/index.js' +import type { WuiListAccordion } from '../composites/wui-list-accordion/index.js' +import type { WuiListContent } from '../composites/wui-list-content/index.js' +import type { WuiListDescription } from '../composites/wui-list-description/index.js' import type { WuiListItem } from '../composites/wui-list-item/index.js' -import type { WuiTransactionListItem } from '../composites/wui-transaction-list-item/index.js' -import type { WuiTransactionListItemLoader } from '../composites/wui-transaction-list-item-loader/index.js' +import type { WuiListNetwork } from '../composites/wui-list-network/index.js' +import type { WuiListToken } from '../composites/wui-list-token/index.js' import type { WuiListWallet } from '../composites/wui-list-wallet/index.js' -import type { WuiLogoSelect } from '../composites/wui-logo-select/index.js' +import type { WuiListWalletTransaction } from '../composites/wui-list-wallet-transaction/index.js' import type { WuiLogo } from '../composites/wui-logo/index.js' +import type { WuiLogoSelect } from '../composites/wui-logo-select/index.js' import type { WuiNetworkButton } from '../composites/wui-network-button/index.js' import type { WuiNetworkImage } from '../composites/wui-network-image/index.js' +import type { WuiNoticeCard } from '../composites/wui-notice-card/index.js' +import type { WuiOnRampActivityItem } from '../composites/wui-onramp-activity-item/index.js' +import type { WuiOnRampProviderItem } from '../composites/wui-onramp-provider-item/index.js' import type { WuiOtp } from '../composites/wui-otp/index.js' +import type { WuiPreviewItem } from '../composites/wui-preview-item/index.js' +import type { WuiProfileButton } from '../composites/wui-profile-button/index.js' +import type { WuiPromo } from '../composites/wui-promo/index.js' import type { WuiQrCode } from '../composites/wui-qr-code/index.js' import type { WuiSearchBar } from '../composites/wui-search-bar/index.js' import type { WuiSnackbar } from '../composites/wui-snackbar/index.js' import type { WuiTabs } from '../composites/wui-tabs/index.js' import type { WuiTag } from '../composites/wui-tag/index.js' +import type { WuiTokenButton } from '../composites/wui-token-button/index.js' +import type { WuiTokenListItem } from '../composites/wui-token-list-item/index.js' import type { WuiTooltip } from '../composites/wui-tooltip/index.js' +import type { WuiTooltipSelect } from '../composites/wui-tooltip-select/index.js' +import type { WuiTransactionListItem } from '../composites/wui-transaction-list-item/index.js' +import type { WuiTransactionListItemLoader } from '../composites/wui-transaction-list-item-loader/index.js' import type { WuiTransactionVisual } from '../composites/wui-transaction-visual/index.js' import type { WuiVisualThumbnail } from '../composites/wui-visual-thumbnail/index.js' import type { WuiWalletImage } from '../composites/wui-wallet-image/index.js' -import type { WuiNoticeCard } from '../composites/wui-notice-card/index.js' -import type { WuiListAccordion } from '../composites/wui-list-accordion/index.js' -import type { WuiListContent } from '../composites/wui-list-content/index.js' -import type { WuiListNetwork } from '../composites/wui-list-network/index.js' -import type { WuiListWalletTransaction } from '../composites/wui-list-wallet-transaction/index.js' -import type { WuiOnRampActivityItem } from '../composites/wui-onramp-activity-item/index.js' -import type { WuiOnRampProviderItem } from '../composites/wui-onramp-provider-item/index.js' -import type { WuiPromo } from '../composites/wui-promo/index.js' -import type { WuiBalance } from '../composites/wui-balance/index.js' -import type { WuiTooltipSelect } from '../composites/wui-tooltip-select/index.js' -import type { WuiProfileButton } from '../composites/wui-profile-button/index.js' -import type { WuiBanner } from '../composites/wui-banner/index.js' -import type { WuiCompatibleNetwork } from '../composites/wui-compatible-network/index.js' -import type { WuiListToken } from '../composites/wui-list-token/index.js' -import type { WuiListDescription } from '../composites/wui-list-description/index.js' -import type { WuiPreviewItem } from '../composites/wui-preview-item/index.js' import type { WuiFlex } from '../layout/wui-flex/index.js' import type { WuiGrid } from '../layout/wui-grid/index.js' @@ -85,11 +93,17 @@ declare global { 'wui-card-select-loader': CustomElement 'wui-card-select': CustomElement 'wui-chip': CustomElement + 'wui-chip-button': CustomElement + 'wui-convert-input': CustomElement + 'wui-convert-details': CustomElement 'wui-connect-button': CustomElement 'wui-cta-button': CustomElement + 'wui-details-group': CustomElement + 'wui-details-group-item': CustomElement 'wui-email-input': CustomElement 'wui-icon-box': CustomElement 'wui-icon-link': CustomElement + 'wui-input-amount': CustomElement 'wui-input-element': CustomElement 'wui-input-numeric': CustomElement 'wui-input-text': CustomElement @@ -108,6 +122,8 @@ declare global { 'wui-snackbar': CustomElement 'wui-tabs': CustomElement 'wui-tag': CustomElement + 'wui-token-button': CustomElement + 'wui-token-list-item': CustomElement 'wui-tooltip': CustomElement 'wui-transaction-visual': CustomElement 'wui-visual-thumbnail': CustomElement diff --git a/packages/ui/src/utils/Math.ts b/packages/ui/src/utils/Math.ts new file mode 100644 index 0000000000..e9f5c615a2 --- /dev/null +++ b/packages/ui/src/utils/Math.ts @@ -0,0 +1,30 @@ +/** + * Interpolates a value from one range to another + * @param inputRange - number array of length 2 that represents the original range + * @param outputRange - number array of length 2 that represents the new range + * @param value - the value to interpolation + * @returns + */ +export function interpolate(inputRange: number[], outputRange: number[], value: number) { + if (inputRange.length !== 2 || outputRange.length !== 2) { + throw new Error('inputRange and outputRange must be an array of length 2') + } + + const originalRangeMin = inputRange[0] || 0 + const originalRangeMax = inputRange[1] || 0 + const newRangeMin = outputRange[0] || 0 + const newRangeMax = outputRange[1] || 0 + + if (value < originalRangeMin) { + return newRangeMin + } + if (value > originalRangeMax) { + return newRangeMax + } + + return ( + ((newRangeMax - newRangeMin) / (originalRangeMax - originalRangeMin)) * + (value - originalRangeMin) + + newRangeMin + ) +} diff --git a/packages/ui/src/utils/NumberUtil.ts b/packages/ui/src/utils/NumberUtil.ts new file mode 100644 index 0000000000..db975b0979 --- /dev/null +++ b/packages/ui/src/utils/NumberUtil.ts @@ -0,0 +1,23 @@ +/** + * Format the given number or string to human readable numbers with the given number of decimals + * @param value - The value to format. It could be a number or string. If it's a string, it will be parsed to a float then formatted. + * @param decimals - number of decimals after dot + * @returns + */ +export function formatNumberToLocalString(value: string | number | undefined, decimals = 2) { + if (value === undefined) { + return '0.00' + } + + if (typeof value === 'number') { + return value.toLocaleString('en-US', { + maximumFractionDigits: decimals, + minimumFractionDigits: decimals + }) + } + + return parseFloat(value).toLocaleString('en-US', { + maximumFractionDigits: decimals, + minimumFractionDigits: decimals + }) +} diff --git a/packages/ui/src/utils/TypeUtil.ts b/packages/ui/src/utils/TypeUtil.ts index 67635919ce..311218713d 100644 --- a/packages/ui/src/utils/TypeUtil.ts +++ b/packages/ui/src/utils/TypeUtil.ts @@ -11,6 +11,7 @@ export type ColorType = | 'inverse-100' | 'success-100' | 'glass-005' + | 'glass-020' export type TextType = | 'large-500' @@ -108,6 +109,7 @@ export type IconType = | 'browser' | 'card' | 'checkmark' + | 'checkmarkBold' | 'chevronBottom' | 'chevronLeft' | 'chevronRight' @@ -146,6 +148,7 @@ export type IconType = | 'swapHorizontal' | 'swapHorizontalBold' | 'swapHorizontalMedium' + | 'swapHorizontalRoundedBold' | 'swapVertical' | 'telegram' | 'twitch' diff --git a/packages/ui/src/utils/UiHelperUtil.ts b/packages/ui/src/utils/UiHelperUtil.ts index 7c09e82a57..7d399687c1 100644 --- a/packages/ui/src/utils/UiHelperUtil.ts +++ b/packages/ui/src/utils/UiHelperUtil.ts @@ -119,5 +119,28 @@ export const UiHelperUtil = { const roundedNumber = Math.abs(number) >= threshold ? Number(number.toFixed(fixed)) : number return roundedNumber + }, + /** + * Format the given number or string to human readable numbers with the given number of decimals + * @param value - The value to format. It could be a number or string. If it's a string, it will be parsed to a float then formatted. + * @param decimals - number of decimals after dot + * @returns + */ + formatNumberToLocalString(value: string | number | undefined, decimals = 2) { + if (value === undefined) { + return '0.00' + } + + if (typeof value === 'number') { + return value.toLocaleString('en-US', { + maximumFractionDigits: decimals, + minimumFractionDigits: decimals + }) + } + + return parseFloat(value).toLocaleString('en-US', { + maximumFractionDigits: decimals, + minimumFractionDigits: decimals + }) } } diff --git a/packages/wagmi/src/client.ts b/packages/wagmi/src/client.ts index 856b97cb35..b6e32bd343 100644 --- a/packages/wagmi/src/client.ts +++ b/packages/wagmi/src/client.ts @@ -8,9 +8,13 @@ import { getEnsName, switchChain, watchAccount, - watchConnectors + watchConnectors, + waitForTransactionReceipt, + estimateGas, + getAccount } from '@wagmi/core' import { mainnet } from 'viem/chains' +import { prepareTransactionRequest, sendTransaction as wagmiSendTransaction } from '@wagmi/core' import type { Chain } from '@wagmi/core/chains' import type { GetAccountReturnType } from '@wagmi/core' import type { @@ -22,8 +26,10 @@ import type { LibraryOptions, NetworkControllerClient, PublicStateControllerState, + SendTransactionArgs, Token } from '@web3modal/scaffold' +import { formatUnits, parseUnits } from 'viem' import type { Hex } from 'viem' import { Web3ModalScaffold } from '@web3modal/scaffold' import type { Web3ModalSIWEClient } from '@web3modal/siwe' @@ -168,7 +174,48 @@ export class Web3Modal extends Web3ModalScaffold { } }, - signMessage: async message => signMessage(this.wagmiConfig, { message }) + signMessage: async message => signMessage(this.wagmiConfig, { message }), + + getEstimatedGas: async args => { + try { + return await estimateGas(this.wagmiConfig, { + account: args.address, + to: args.to, + data: args.data, + type: 'legacy' + }) + } catch (error) { + return 0n + } + }, + + sendTransaction: async (data: SendTransactionArgs) => { + const { chainId } = getAccount(this.wagmiConfig) + + const txParams = { + account: data.address, + to: data.to, + value: data.value, + gas: data.gas, + gasPrice: data.gasPrice, + data: data.data, + chainId, + type: 'legacy' as const + } + + // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error, @typescript-eslint/ban-ts-comment + // @ts-ignore + await prepareTransactionRequest(this.wagmiConfig, txParams) + const tx = await wagmiSendTransaction(this.wagmiConfig, txParams) + + await waitForTransactionReceipt(this.wagmiConfig, { hash: tx, timeout: 25000 }) + + return tx + }, + + parseUnits, + + formatUnits } super({ @@ -435,7 +482,13 @@ export class Web3Modal extends Web3ModalScaffold { provider.onRpcRequest(request => { if (W3mFrameHelpers.checkIfRequestExists(request)) { if (!W3mFrameHelpers.checkIfRequestIsAllowed(request)) { - super.open({ view: 'ApproveTransaction' }) + if (super.isOpen()) { + if (!super.isTransactionStackEmpty()) { + super.redirect('ApproveTransaction') + } + } else { + super.open({ view: 'ApproveTransaction' }) + } } } else { super.open() @@ -449,8 +502,33 @@ export class Web3Modal extends Web3ModalScaffold { } }) - provider.onRpcResponse(() => { - super.close() + provider.onRpcResponse(receive => { + // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error, @typescript-eslint/ban-ts-comment + // @ts-ignore + const payload = receive?.payload + // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error, @typescript-eslint/ban-ts-comment + // @ts-ignore + const isError = receive?.type === '@w3m-frame/RPC_REQUEST_ERROR' + + if (isError && super.isOpen()) { + if (super.isTransactionStackEmpty()) { + super.close() + } else { + super.popTransactionStack(true) + } + } + + const isPayloadString = typeof payload === 'string' + const isAddress = isPayloadString ? payload?.startsWith('0x') : false + const isCompleted = isAddress && payload?.length > 10 + + if (isCompleted) { + if (super.isTransactionStackEmpty()) { + super.close() + } else { + super.popTransactionStack() + } + } }) provider.onNotConnected(() => {