From f34ec0d9cc40c3b425011ce1117e0f70206ec064 Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz <43585069+Sharqiewicz@users.noreply.github.com> Date: Wed, 31 Jul 2024 12:33:16 +0200 Subject: [PATCH] Implement New Vortex UI (#66) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * implement basic styles of navigation and swap * add Connect Wallet button * implement PoolListModals and buttons * add wallet icon * implement collapse chevron * implement desktop UI * add basic form validation * improve modal * fix bugs and clean code * implement mobile responsiveness and fix bugs * remove console log * improve code readability * Fix prop errors * Amend merge * import main .css to new index component * Move css import to app.tsx * add select chain option for ChainName component * improve code readability * fix import * adjust useGetIcon to return current chain token icon * fix useGetIcon hook default * remove eurc * use useGetIcon for PoolList tokens * show fees in $ * add logic for disabling submit button * fix FeeCollapse currencies * update error handling * Progress screen * add progress screen * add beforeunload confirmation prompt * Flow continuable (#70) * Use of pen ephemeral account, trigger transfer when tokens received * Phase 6b (#44) * Execute Nabla after SEP-24 * Some fixes * Fix `yarn test` not working by importing a preset that enhances transpiling vite code * Fix errors: useQuery result must not be undefined * Update min offramp amount for nTokens * Fix typo --------- Co-authored-by: Marcel Ebert * use pen ephemeral address in performSwap * Refactor wagmi config * Add contract abis * Implement swap with squid router * wip * Refactor squidrouter service * Small refactoring * Use swap in Inputkeys * Fix transaction status issues * cleanup old prototype logic * wip. testing integration of squidrouter * Make USDC the default selection * Delete duplicate /contracts/Erc20.ts * format, remove abi duplicate * testing ephemeral funding * Add logic to fund ephemeral account on Pendulum * Change USDC ERC20 address * Add funding account phrase * Fix signing transaction to fund ephemeral acc * trigger funding of eph. account after swap, fix deposit event listener * wip. testing * Fix lint errors * Use USDC instead of USDC.e * Increase funding account * Add quickfix with apiplus * Use public horizon in signing service * Fix errors * Remove unused wallet components * Change slippageBasisPoints from 20 to 30 * crop input box for mobile * cleanup pendulum ephemeral * Fix styling for mobile * Remove media query to avoid conflicts * remove token dust * fix for clean pen ephemeral * use new integrator id and plus squid route * use getROuteApiPlus function * Remove message about minimum EURC amount * Change button for anchor to a element * attempt to transfer all tokens regardless of balance * log ephemeral account on offramp * log ephemeral account after sep is completed * WIP initial refactoring * integrating logic into new UI * improving data flow, recover pendulum ephemeral * mock sep6, restore pen ephemeral from seed * recovering from squidrouter intermediate steps * testing recovery in squidrouter process, funding account * nabla, stellar operations and redeem modifications for recoverability * add sep24 again * remove example transaction request data * recover stellar keys after sep10, save stellar ephemeral keys * fix, store only secret for stellar keys * remove extra logs * start sep24 second waiting loop as soon as first is initiated * remove unused states * reorganize recovery params restore * Cleanup continuation flow * Try to fix dependencies * Fix inconsistent versions * Revert package.json and yarn.lock * Implement change requests * Fix bugs in offramping flow * Implement change requests * Remove metamask base code --------- Co-authored-by: Torsten Stüber <15174476+TorstenStueber@users.noreply.github.com> Co-authored-by: Marcel Ebert Co-authored-by: bogdanS98 Co-authored-by: Kacper Szarkiewicz Co-authored-by: Kacper Szarkiewicz <43585069+Sharqiewicz@users.noreply.github.com> --------- Co-authored-by: Marcel Ebert Co-authored-by: Gianfranco Co-authored-by: gianfra-t <96739519+gianfra-t@users.noreply.github.com> Co-authored-by: Torsten Stüber <15174476+TorstenStueber@users.noreply.github.com> Co-authored-by: bogdanS98 --- App.css | 387 +------ index.html | 6 +- package.json | 5 +- postcss.config.cjs | 8 + postcss.config.js | 6 - signer-service/.env.example | 2 +- signer-service/yarn.lock | 954 ++++++++---------- src/GlobalStateProvider.tsx | 11 +- src/app.tsx | 16 +- src/assets/CloseIcon.tsx | 11 + src/assets/coins/USDC_POLYGON.svg | 14 + src/assets/logo/white.png | Bin 0 -> 2283 bytes src/components/AssetNumericInput/index.tsx | 69 ++ src/components/BenefitsList/index.tsx | 37 + src/components/Box/index.tsx | 12 + src/components/Dialog/index.tsx | 96 ++ src/components/ExchangeRate/index.tsx | 22 + src/components/FeeCollapse/index.tsx | 71 ++ src/components/GenericEvent.tsx | 2 + src/components/InputKeys/AmountSelector.tsx | 149 --- src/components/InputKeys/From.tsx | 97 -- .../InputKeys/PoolListItem/index.tsx | 44 + src/components/InputKeys/SelectionModal.tsx | 141 +-- src/components/InputKeys/To.tsx | 139 --- src/components/InputKeys/index.tsx | 259 ----- src/components/LabeledInput/index.tsx | 13 + src/components/Nabla/BalanceState.tsx | 76 -- src/components/Nabla/NumberInput.tsx | 44 - src/components/Nabla/TokenBalance.tsx | 30 - src/components/Nabla/schema.tsx | 24 +- src/components/Nabla/useSwapForm.tsx | 48 +- src/components/Navbar/index.tsx | 109 ++ .../NumericInput/NumericInput.test.tsx | 110 ++ src/components/NumericInput/index.tsx | 83 ++ src/components/PublicKey.tsx | 118 --- src/components/PublicKey/index.tsx | 118 --- src/components/Sep24Component.tsx | 44 - src/components/Skeleton/index.tsx | 15 - src/components/TextInput/index.tsx | 43 + src/components/buttons/AssetButton/index.tsx | 25 + src/components/buttons/CloseButton/index.tsx | 19 + .../buttons/ConnectWallet/index.tsx | 67 ++ .../buttons/SwapSubmitButton/index.tsx | 40 + src/config/index.ts | 6 +- src/constants/cache.ts | 2 +- src/constants/localStorage.ts | 11 + src/constants/tokenConfig.ts | 136 +-- src/helpers/contracts.ts | 5 +- src/helpers/parseNumbers.ts | 41 +- src/helpers/substrate.ts | 56 - src/hooks/nabla/useContractRead.ts | 6 +- src/hooks/nabla/useTokenAmountOut.ts | 63 +- src/hooks/useBoolean.ts | 20 - src/hooks/useClipboard.ts | 18 - src/hooks/useGetIcon.tsx | 38 + src/hooks/useMainProcess.ts | 99 ++ src/layouts/index.tsx | 15 + src/pages/landing/index.tsx | 308 ------ src/pages/progress/index.tsx | 34 + src/pages/swap/index.tsx | 298 ++++++ src/pages/swap/sections/BankDetails.tsx | 26 + src/queries/constants.ts | 9 - src/queries/helpers.ts | 30 - src/services/anchor/index.ts | 123 ++- src/services/evmTransactions.ts | 6 + src/services/localStorage.tsx | 39 - src/services/metamask/index.ts | 140 --- src/services/nabla.ts | 188 ++-- src/services/offrampingFlow.ts | 197 ++++ .../polkadot/__tests__/spacewalk.test.tsx | 10 +- src/services/polkadot/ephemeral.tsx | 229 +++-- src/services/polkadot/eventListener.tsx | 13 +- src/services/polkadot/index.tsx | 182 +++- src/services/polkadot/polkadotApi.tsx | 12 +- src/services/polkadot/spacewalk.tsx | 16 +- src/services/polkadot/utils.tsx | 22 - src/services/signingService.tsx | 2 +- src/services/squidrouter/config.ts | 6 +- src/services/squidrouter/index.tsx | 174 ---- src/services/squidrouter/process.ts | 60 ++ src/services/squidrouter/route.ts | 121 +-- src/services/stellar/index.tsx | 150 +-- src/services/stellar/utils.tsx | 19 +- src/services/storage/local.ts | 1 + src/types/index.ts | 30 + yarn.lock | 84 +- 86 files changed, 3046 insertions(+), 3583 deletions(-) create mode 100644 postcss.config.cjs delete mode 100644 postcss.config.js create mode 100644 src/assets/CloseIcon.tsx create mode 100644 src/assets/coins/USDC_POLYGON.svg create mode 100644 src/assets/logo/white.png create mode 100644 src/components/AssetNumericInput/index.tsx create mode 100644 src/components/BenefitsList/index.tsx create mode 100644 src/components/Box/index.tsx create mode 100644 src/components/Dialog/index.tsx create mode 100644 src/components/ExchangeRate/index.tsx create mode 100644 src/components/FeeCollapse/index.tsx delete mode 100644 src/components/InputKeys/AmountSelector.tsx delete mode 100644 src/components/InputKeys/From.tsx create mode 100644 src/components/InputKeys/PoolListItem/index.tsx delete mode 100644 src/components/InputKeys/To.tsx delete mode 100644 src/components/InputKeys/index.tsx create mode 100644 src/components/LabeledInput/index.tsx delete mode 100644 src/components/Nabla/BalanceState.tsx delete mode 100644 src/components/Nabla/NumberInput.tsx delete mode 100644 src/components/Nabla/TokenBalance.tsx create mode 100644 src/components/Navbar/index.tsx create mode 100644 src/components/NumericInput/NumericInput.test.tsx create mode 100644 src/components/NumericInput/index.tsx delete mode 100644 src/components/PublicKey.tsx delete mode 100644 src/components/PublicKey/index.tsx delete mode 100644 src/components/Sep24Component.tsx delete mode 100644 src/components/Skeleton/index.tsx create mode 100644 src/components/TextInput/index.tsx create mode 100644 src/components/buttons/AssetButton/index.tsx create mode 100644 src/components/buttons/CloseButton/index.tsx create mode 100644 src/components/buttons/ConnectWallet/index.tsx create mode 100644 src/components/buttons/SwapSubmitButton/index.tsx delete mode 100644 src/helpers/substrate.ts delete mode 100644 src/hooks/useBoolean.ts delete mode 100644 src/hooks/useClipboard.ts create mode 100644 src/hooks/useGetIcon.tsx create mode 100644 src/hooks/useMainProcess.ts create mode 100644 src/layouts/index.tsx delete mode 100644 src/pages/landing/index.tsx create mode 100644 src/pages/progress/index.tsx create mode 100644 src/pages/swap/index.tsx create mode 100644 src/pages/swap/sections/BankDetails.tsx delete mode 100644 src/queries/constants.ts delete mode 100644 src/queries/helpers.ts create mode 100644 src/services/evmTransactions.ts delete mode 100644 src/services/localStorage.tsx delete mode 100644 src/services/metamask/index.ts create mode 100644 src/services/offrampingFlow.ts delete mode 100644 src/services/polkadot/utils.tsx delete mode 100644 src/services/squidrouter/index.tsx create mode 100644 src/services/squidrouter/process.ts create mode 100644 src/types/index.ts diff --git a/App.css b/App.css index b6c41bb3..f74adcff 100644 --- a/App.css +++ b/App.css @@ -1,391 +1,30 @@ -:root { - --border-light: #eff2f5; - --nice-light: #907ea0; - --clear-light: #ffffff; - --footer-links-light: #58667e; - --nav-arrow-light: var(--clear-light); - --nice-dark: #09c63e; - --footer-links-dark: #616875; - --nav-arrow-dark: hsla(271, 25%, 19%, 1); - --darkner: #1c1c1c; - --dark-light: #202020; - - /* From daisyUI theme variables*/ - --text-primary: var(--bc); - --text-secondary: var(--nc); - --table-background: #1c1c1c; -} - -[data-theme='amplitude'] { - --primary: #4ee59a; - --border: var(--border-dark); - --nice: var(--nice-dark); - --footer-links: var(--footer-links-dark); - --card-border: #434343; - --table-text: #bbbbbb; - --table-text-header: #fff; - --table-background: #1c1c1c; - --table-border: #242424; - --modal-title: var(--clear-light); - --portfolio-bg: var(--dark-light); - --portfolio-border: rgba(229, 229, 229, 0.03); - --selected-nav-item: hsla(158, 100%, 47%, 0.08); - --subtitle-collator-box: #fff; - --text-primary-disabled: rgba(88, 102, 126, 0.4); - --tag-background: rgba(78, 229, 154, 0.16); /* --primary 16% */ - --network-bg: #252733; - --scroll-track: rgba(0, 0, 0, 0.5); - --scroll-bg: rgba(255, 255, 255, 0.3); -} - -[data-theme='pendulum'] { - --primary: #907ea0; - --border: var(--border-light); - --nice: var(--nice-dark); - --footer-links: var(--footer-links-light); - --card-border: #dfe0eb; - --table-text: #252733; - --table-text-header: #9fa2b4; - --table-background: #fff; - --table-border: #dfe0eb; - --modal-title: var(--darkner); - --portfolio-bg: var(--clear-light); - --portfolio-border: #e5e5e5; - --subtitle-collator-box: #828282; - --selected-nav-item: hsla(272, 15%, 56%, 0.1); - --text-primary-disabled: rgba(88, 102, 126, 0.4); - --tag-background: rgba(144, 126, 160, 0.16); /* --primary 16% */ - --network-bg: hsla(0, 0%, 0%, 0.04); - --scroll-track: rgba(0, 0, 0, 0.12); - --scroll-bg: rgba(0, 0, 0, 0.25); -} - -::-webkit-scrollbar { - position: relative; - width: 10px; - height: 10px; - background-color: transparent; - z-index: 9999999; -} -::-webkit-scrollbar-track { - -webkit-box-shadow: inset 0 0 6px var(--scroll-track); - background-color: transparent; -} -::-webkit-scrollbar-thumb { - border-radius: 5px; - -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.1); - background-color: var(--scroll-bg); -} - html, body { margin: 0; padding: 0; - width: 100%; - height: 100%; - display: flex; - justify-content: center; - overflow-x: hidden; - - /* Important to make sure the WalletConnect dialog is shown on top of the others */ - --wcm-z-index: 1000; -} - -#app { - width: 100%; - box-sizing: border-box; -} - -.card { - width: 100%; - max-width: 640px; - background-color: #e5e5e5; - border: 1px solid #d1d5db; - border-radius: 0.75rem; - box-shadow: 0 0 #0000, 0 0 #0000, 0 1px 2px 0 rgba(0, 0, 0, 0.05); -} - -.card.card-bordered { - border: 1px solid var(--card-border); -} - -.card-body { - padding: 1.25rem; -} - -.inputBox { - width: 40%; - margin: 20px auto; - padding: 20px; - border: 1px solid #ccc; - box-shadow: 0 0 10px #ccc; - transition: all 0.5s ease-in-out; - display: flex; - flex-direction: column; - align-items: center; -} - -.inputBox.active { - margin-top: 5px; -} - -input { - margin: 10px; - padding: 10px; -} - -.eventBox { - border: 1px solid #ccc; - box-shadow: 0 0 10px #ccc; - transition: all 0.5s ease-in-out; - color: #333; - word-wrap: break-word; - text-align: center; -} - -.eventBox.waiting { - background-color: #ffffff; - border-color: #ccc; -} - -.eventBox.active.waiting { - /*animation: rotateShadowBlue 5s linear infinite;*/ - border-color: #ffffff; + font-family: 'Red Hat Display', sans-serif; } -.eventBox.success { - background-color: #d4edda; - border-color: #c3e6cb; +#app, +div[data-rk] { + min-height: 100vh; } -.eventBox.active { - transform: scale(1.2); - background-color: #f0f0f0; -} - -.eventBox.error { - border: 1px solid #ccc; - box-shadow: 0 0 10px #ccc; - transition: all 0.5s ease-in-out; - color: red; - background-color: #f8d7da; - border-color: #f5c6cb; - color: red; -} - -.iframe-container { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; +.shadow-custom { + box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); } @tailwind base; @tailwind components; @tailwind utilities; -*, -*:before, -*:after { - box-sizing: border-box; -} - -body, -input, -textarea, -select { - font-family: 'Nunito Sans', sans-serif; -} - -.font-outfit { - font-family: 'Outfit', sans-serif; -} - -strong { - font-weight: 700; -} - -input::-webkit-outer-spin-button, -input::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; -} - -/* Firefox */ -input[type='number'] { - --moz-appearance: textfield; -} - -/* utility classes */ -.center { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - -/* theme override */ -.card { - border-radius: 0.75rem; -} -.card.card-bordered { - border: 1px solid var(--card-border); -} - -.btn, -.input { - line-height: 1.5; -} - -.input-ghost:focus { - color: inherit; -} - -.collapse-title, -:where(.collapse > input[type='checkbox']) { - min-height: 0; - height: 100%; -} -.modal.modal-top { - align-items: flex-start; -} -.input-ghost:disabled, -.input-ghost { - background-color: transparent; - outline: none; - border: 0; -} - -.btn-disabled, -.btn-disabled:hover, -.btn[disabled], -.btn[disabled]:hover { - --tw-bg-opacity: 1; - --tw-text-opacity: 1; - background-color: hsl(var(--s)); - opacity: 0.3; -} - -w3m-modal { - position: relative; - z-index: 1000; -} - -@keyframes bounce-x { - 0%, - 100% { - transform: translateX(0%); - animation-timing-function: cubic-bezier(0.8, 0, 1, 1); - } - 50% { - transform: translateX(25%); - animation-timing-function: cubic-bezier(0, 0, 0.2, 1); - } -} - -.animate-bounce-x { - animation: bounce-x 1s infinite; -} - -.badge { - height: 25px; - white-space: nowrap; -} - -@layer components { - .badge-secondary-content { - @apply bg-secondary-content; - } -} - -.description { - color: #666; - font-size: 14px; - margin-bottom: 10px; -} - -.icons { - display: flex; - justify-content: center; - align-items: center; - margin-bottom: 20px; -} - -.icon { - width: 30px; - height: 30px; -} - -.arrow { - width: 50px; - margin: 0 10px; -} - -.wallet-info { - display: flex; - flex-direction: column; - align-items: center; - padding: 10px; - background-color: #f0f0f0; - border: 1px solid #dcdcdc; -} - -.wallet-header { - display: flex; - align-items: center; - margin-bottom: 10px; -} - -.wallet-icon { - width: 20px; - height: 20px; - margin-right: 10px; -} - -.wallet-address { - font-size: 14px; -} - -.disconnect-btn { - background-color: #ff4d4f; - color: white; -} - -.connect-btn { - min-height: 2.1rem; - height: auto; - padding: 0.5rem 1rem; -} - -.modal-select { - max-height: 600px; -} - -/* Fix wallet select modal to allow for scrolling */ -#react-portal-modal-container main { - overflow-y: auto; -} - -#react-portal-modal-container > div > div { - max-height: 90vh; -} - -.offramp-started { - font-size: 1.2rem; - color: rgb(0, 0, 0); - margin-top: 20px; +:root:has(:is(.modal-open, .modal:target, .modal-toggle:checked + .modal, .modal[open])) { + scrollbar-gutter: unset; } -.general-service-error-message { - position: fixed; - top: 20%; - left: 50%; - transform: translate(-50%, -50%); - padding: 20px; - background-color: rgb(154, 63, 63); - color: white; - font-size: 16px; - margin-top: 3%; - border-radius: 8px; - z-index: 1000; +.input-disabled { + cursor: not-allowed; + color: rgb(229 231 235 / var(--tw-text-opacity)); + border-color: var(--fallback-b2, oklch(var(--b2) / var(--tw-border-opacity))); + background-color: var(--fallback-b2, oklch(var(--b2) / var(--tw-bg-opacity))); } diff --git a/index.html b/index.html index 4a201ddf..0d8addc4 100644 --- a/index.html +++ b/index.html @@ -4,13 +4,17 @@ - PendulumPay + Vortex + diff --git a/package.json b/package.json index 4e2db19f..19cdd27a 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,6 @@ "postinstall": "husky install" }, "dependencies": { - "@chainsafe/metamask-polkadot-adapter": "^0.6.0", - "@chainsafe/metamask-polkadot-types": "^0.7.0", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@fontsource/roboto": "^5.0.8", @@ -53,6 +51,7 @@ "bn.js": "^5.2.1", "buffer": "^6.0.3", "daisyui": "^4.11.1", + "framer-motion": "^11.2.14", "postcss": "^8.4.38", "preact": "^10.12.1", "react-daisyui": "^5.0.0", @@ -82,11 +81,11 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/preact": "^3.2.3", "@testing-library/preact-hooks": "^1.1.0", + "@testing-library/user-event": "^14.5.2", "@types/big.js": "^6", "@types/bn.js": "^5", "@types/jest": "^29.4.0", "@types/node": "^18.14.1", - "@types/react": "^18.0.28", "@types/testing-library__jest-dom": "^5.14.5", "@typescript-eslint/eslint-plugin": "^5.53.0", "@typescript-eslint/parser": "^5.53.0", diff --git a/postcss.config.cjs b/postcss.config.cjs new file mode 100644 index 00000000..e569373f --- /dev/null +++ b/postcss.config.cjs @@ -0,0 +1,8 @@ +module.exports = { + plugins: { + 'postcss-import': {}, + 'tailwindcss/nesting': {}, + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/postcss.config.js b/postcss.config.js deleted file mode 100644 index 2aa7205d..00000000 --- a/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; diff --git a/signer-service/.env.example b/signer-service/.env.example index 254778c5..dec1c6f4 100644 --- a/signer-service/.env.example +++ b/signer-service/.env.example @@ -3,4 +3,4 @@ PORT=3000 RATE_LIMIT_WINDOW_MINUTES=15 RATE_LIMIT_MAX_REQUESTS=100 RATE_LIMIT_NUMBER_OF_PROXIES=1 -FUNDING_SECRET=SCVJD7BHU5LNFXNIDC7E226HISKUOZUEPJWLA2YU2GNBFMP5PYF2TQBH \ No newline at end of file +FUNDING_SECRET=SAKRXYB5EVEQFGQBYCCRYTUVX3U35AX3USQQD3J32EHUTSYQHHSB7GSF \ No newline at end of file diff --git a/signer-service/yarn.lock b/signer-service/yarn.lock index 440f7911..51f53054 100644 --- a/signer-service/yarn.lock +++ b/signer-service/yarn.lock @@ -5,13 +5,6 @@ __metadata: version: 8 cacheKey: 10 -"@aashutoshrathi/word-wrap@npm:^1.2.3": - version: 1.2.6 - resolution: "@aashutoshrathi/word-wrap@npm:1.2.6" - checksum: 6eebd12a5cd03cee38fcb915ef9f4ea557df6a06f642dfc7fe8eb4839eb5c9ca55a382f3604d52c14200b0c214c12af5e1f23d2a6d8e23ef2d016b105a9d6c0a - languageName: node - linkType: hard - "@ampproject/remapping@npm:^2.2.0": version: 2.3.0 resolution: "@ampproject/remapping@npm:2.3.0" @@ -31,238 +24,241 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.23.5, @babel/code-frame@npm:^7.24.1, @babel/code-frame@npm:^7.24.2": - version: 7.24.2 - resolution: "@babel/code-frame@npm:7.24.2" +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/code-frame@npm:7.24.7" dependencies: - "@babel/highlight": "npm:^7.24.2" + "@babel/highlight": "npm:^7.24.7" picocolors: "npm:^1.0.0" - checksum: 7db8f5b36ffa3f47a37f58f61e3d130b9ecad21961f3eede7e2a4ac2c7e4a5efb6e9d03a810c669bc986096831b6c0dfc2c3082673d93351b82359c1b03e0590 + checksum: 4812e94885ba7e3213d49583a155fdffb05292330f0a9b2c41b49288da70cf3c746a3fda0bf1074041a6d741c33f8d7be24be5e96f41ef77395eeddc5c9ff624 languageName: node linkType: hard -"@babel/compat-data@npm:^7.23.5": - version: 7.24.4 - resolution: "@babel/compat-data@npm:7.24.4" - checksum: e51faec0ac8259f03cc5029d2b4a944b4fee44cb5188c11530769d5beb81f384d031dba951febc3e33dbb48ceb8045b1184f5c1ac4c5f86ab1f5e951e9aaf7af +"@babel/compat-data@npm:^7.24.8": + version: 7.24.9 + resolution: "@babel/compat-data@npm:7.24.9" + checksum: fcdbf3dd978305880f06ae20a23f4f68a8eddbe64fc5d2fbc98dfe4cdf15c174cff41e3a8eb9d935f9f3a68d3a23fa432044082ee9768a2ed4b15f769b8f6853 languageName: node linkType: hard "@babel/core@npm:^7.7.5": - version: 7.24.4 - resolution: "@babel/core@npm:7.24.4" + version: 7.24.9 + resolution: "@babel/core@npm:7.24.9" dependencies: "@ampproject/remapping": "npm:^2.2.0" - "@babel/code-frame": "npm:^7.24.2" - "@babel/generator": "npm:^7.24.4" - "@babel/helper-compilation-targets": "npm:^7.23.6" - "@babel/helper-module-transforms": "npm:^7.23.3" - "@babel/helpers": "npm:^7.24.4" - "@babel/parser": "npm:^7.24.4" - "@babel/template": "npm:^7.24.0" - "@babel/traverse": "npm:^7.24.1" - "@babel/types": "npm:^7.24.0" + "@babel/code-frame": "npm:^7.24.7" + "@babel/generator": "npm:^7.24.9" + "@babel/helper-compilation-targets": "npm:^7.24.8" + "@babel/helper-module-transforms": "npm:^7.24.9" + "@babel/helpers": "npm:^7.24.8" + "@babel/parser": "npm:^7.24.8" + "@babel/template": "npm:^7.24.7" + "@babel/traverse": "npm:^7.24.8" + "@babel/types": "npm:^7.24.9" convert-source-map: "npm:^2.0.0" debug: "npm:^4.1.0" gensync: "npm:^1.0.0-beta.2" json5: "npm:^2.2.3" semver: "npm:^6.3.1" - checksum: 1e049f8df26be0fe5be36173fd7c33dfb004eeeec28152fea83c90e71784f9a6f2237296f43a2ee7d9041e2a33a05f43da48ce2d4e0cd473a682328ca07ce7e0 + checksum: f00a372fa547f6e21f4db1b6e521e6eb01f77f5931726897aae6f4cf29a687f615b9b77147b539e851a68bf94e4850bcfba7eb11091dd8e2bc625f6d831ce257 languageName: node linkType: hard -"@babel/generator@npm:^7.24.1, @babel/generator@npm:^7.24.4": - version: 7.24.4 - resolution: "@babel/generator@npm:7.24.4" +"@babel/generator@npm:^7.24.8, @babel/generator@npm:^7.24.9": + version: 7.24.10 + resolution: "@babel/generator@npm:7.24.10" dependencies: - "@babel/types": "npm:^7.24.0" + "@babel/types": "npm:^7.24.9" "@jridgewell/gen-mapping": "npm:^0.3.5" "@jridgewell/trace-mapping": "npm:^0.3.25" jsesc: "npm:^2.5.1" - checksum: 69e1772dcf8f95baec951f422cca091d59a3f29b5eedc989ad87f7262289b94625983f6fe654302ca17aae0a32f9232332b83fcc85533311d6267b09c58b1061 + checksum: c2491fb7d985527a165546cbcf9e5f6a2518f2a968c7564409c012acce1019056b21e67a152af89b3f4d4a295ca2e75a1a16858152f750efbc4b5087f0cb7253 languageName: node linkType: hard -"@babel/helper-compilation-targets@npm:^7.23.6": - version: 7.23.6 - resolution: "@babel/helper-compilation-targets@npm:7.23.6" +"@babel/helper-compilation-targets@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/helper-compilation-targets@npm:7.24.8" dependencies: - "@babel/compat-data": "npm:^7.23.5" - "@babel/helper-validator-option": "npm:^7.23.5" - browserslist: "npm:^4.22.2" + "@babel/compat-data": "npm:^7.24.8" + "@babel/helper-validator-option": "npm:^7.24.8" + browserslist: "npm:^4.23.1" lru-cache: "npm:^5.1.1" semver: "npm:^6.3.1" - checksum: 05595cd73087ddcd81b82d2f3297aac0c0422858dfdded43d304786cf680ec33e846e2317e6992d2c964ee61d93945cbf1fa8ec80b55aee5bfb159227fb02cb9 + checksum: 3489280d07b871af565b32f9b11946ff9a999fac0db9bec5df960760f6836c7a4b52fccb9d64229ccce835d37a43afb85659beb439ecedde04dcea7eb062a143 languageName: node linkType: hard -"@babel/helper-environment-visitor@npm:^7.22.20": - version: 7.22.20 - resolution: "@babel/helper-environment-visitor@npm:7.22.20" - checksum: d80ee98ff66f41e233f36ca1921774c37e88a803b2f7dca3db7c057a5fea0473804db9fb6729e5dbfd07f4bed722d60f7852035c2c739382e84c335661590b69 +"@babel/helper-environment-visitor@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-environment-visitor@npm:7.24.7" + dependencies: + "@babel/types": "npm:^7.24.7" + checksum: 079d86e65701b29ebc10baf6ed548d17c19b808a07aa6885cc141b690a78581b180ee92b580d755361dc3b16adf975b2d2058b8ce6c86675fcaf43cf22f2f7c6 languageName: node linkType: hard -"@babel/helper-function-name@npm:^7.23.0": - version: 7.23.0 - resolution: "@babel/helper-function-name@npm:7.23.0" +"@babel/helper-function-name@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-function-name@npm:7.24.7" dependencies: - "@babel/template": "npm:^7.22.15" - "@babel/types": "npm:^7.23.0" - checksum: 7b2ae024cd7a09f19817daf99e0153b3bf2bc4ab344e197e8d13623d5e36117ed0b110914bc248faa64e8ccd3e97971ec7b41cc6fd6163a2b980220c58dcdf6d + "@babel/template": "npm:^7.24.7" + "@babel/types": "npm:^7.24.7" + checksum: 2ceb3d9b2b35a0fc4100fc06ed7be3bc38f03ff0bf128ff0edbc0cc7dd842967b1496fc70b5c616c747d7711c2b87e7d025c8888f48740631d6148a9d3614f85 languageName: node linkType: hard -"@babel/helper-hoist-variables@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-hoist-variables@npm:7.22.5" +"@babel/helper-hoist-variables@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-hoist-variables@npm:7.24.7" dependencies: - "@babel/types": "npm:^7.22.5" - checksum: 394ca191b4ac908a76e7c50ab52102669efe3a1c277033e49467913c7ed6f7c64d7eacbeabf3bed39ea1f41731e22993f763b1edce0f74ff8563fd1f380d92cc + "@babel/types": "npm:^7.24.7" + checksum: 6cfdcf2289cd12185dcdbdf2435fa8d3447b797ac75851166de9fc8503e2fd0021db6baf8dfbecad3753e582c08e6a3f805c8d00cbed756060a877d705bd8d8d languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.22.15": - version: 7.24.3 - resolution: "@babel/helper-module-imports@npm:7.24.3" +"@babel/helper-module-imports@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-module-imports@npm:7.24.7" dependencies: - "@babel/types": "npm:^7.24.0" - checksum: 42fe124130b78eeb4bb6af8c094aa749712be0f4606f46716ce74bc18a5ea91c918c547c8bb2307a2e4b33f163e4ad2cb6a7b45f80448e624eae45b597ea3499 + "@babel/traverse": "npm:^7.24.7" + "@babel/types": "npm:^7.24.7" + checksum: df8bfb2bb18413aa151ecd63b7d5deb0eec102f924f9de6bc08022ced7ed8ca7fed914562d2f6fa5b59b74a5d6e255dc35612b2bc3b8abf361e13f61b3704770 languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/helper-module-transforms@npm:7.23.3" +"@babel/helper-module-transforms@npm:^7.24.9": + version: 7.24.9 + resolution: "@babel/helper-module-transforms@npm:7.24.9" dependencies: - "@babel/helper-environment-visitor": "npm:^7.22.20" - "@babel/helper-module-imports": "npm:^7.22.15" - "@babel/helper-simple-access": "npm:^7.22.5" - "@babel/helper-split-export-declaration": "npm:^7.22.6" - "@babel/helper-validator-identifier": "npm:^7.22.20" + "@babel/helper-environment-visitor": "npm:^7.24.7" + "@babel/helper-module-imports": "npm:^7.24.7" + "@babel/helper-simple-access": "npm:^7.24.7" + "@babel/helper-split-export-declaration": "npm:^7.24.7" + "@babel/helper-validator-identifier": "npm:^7.24.7" peerDependencies: "@babel/core": ^7.0.0 - checksum: 583fa580f8e50e6f45c4f46aa76a8e49c2528deb84e25f634d66461b9a0e2420e13979b0a607b67aef67eaf8db8668eb9edc038b4514b16e3879fe09e8fd294b + checksum: eaed9cb93edb11626758f76bfb482f9c3b6583f6756813c5ef849d6d52bbe7c2cb39f61646758e860732d14c2588b60eb4e2af78d7751450649a8d3d7ca41697 languageName: node linkType: hard -"@babel/helper-simple-access@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-simple-access@npm:7.22.5" +"@babel/helper-simple-access@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-simple-access@npm:7.24.7" dependencies: - "@babel/types": "npm:^7.22.5" - checksum: 7d5430eecf880937c27d1aed14245003bd1c7383ae07d652b3932f450f60bfcf8f2c1270c593ab063add185108d26198c69d1aca0e6fb7c6fdada4bcf72ab5b7 + "@babel/traverse": "npm:^7.24.7" + "@babel/types": "npm:^7.24.7" + checksum: 5083e190186028e48fc358a192e4b93ab320bd016103caffcfda81302a13300ccce46c9cd255ae520c25d2a6a9b47671f93e5fe5678954a2329dc0a685465c49 languageName: node linkType: hard -"@babel/helper-split-export-declaration@npm:^7.22.6": - version: 7.22.6 - resolution: "@babel/helper-split-export-declaration@npm:7.22.6" +"@babel/helper-split-export-declaration@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-split-export-declaration@npm:7.24.7" dependencies: - "@babel/types": "npm:^7.22.5" - checksum: e141cace583b19d9195f9c2b8e17a3ae913b7ee9b8120246d0f9ca349ca6f03cb2c001fd5ec57488c544347c0bb584afec66c936511e447fd20a360e591ac921 + "@babel/types": "npm:^7.24.7" + checksum: ff04a3071603c87de0d6ee2540b7291ab36305b329bd047cdbb6cbd7db335a12f9a77af1cf708779f75f13c4d9af46093c00b34432e50b2411872c658d1a2e5e languageName: node linkType: hard -"@babel/helper-string-parser@npm:^7.23.4": - version: 7.24.1 - resolution: "@babel/helper-string-parser@npm:7.24.1" - checksum: 04c0ede77b908b43e6124753b48bc485528112a9335f0a21a226bff1ace75bb6e64fab24c85cb4b1610ef3494dacd1cb807caeb6b79a7b36c43d48c289b35949 +"@babel/helper-string-parser@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/helper-string-parser@npm:7.24.8" + checksum: 6d1bf8f27dd725ce02bdc6dffca3c95fb9ab8a06adc2edbd9c1c9d68500274230d1a609025833ed81981eff560045b6b38f7b4c6fb1ab19fc90e5004e3932535 languageName: node linkType: hard -"@babel/helper-validator-identifier@npm:^7.22.20": - version: 7.22.20 - resolution: "@babel/helper-validator-identifier@npm:7.22.20" - checksum: df882d2675101df2d507b95b195ca2f86a3ef28cb711c84f37e79ca23178e13b9f0d8b522774211f51e40168bf5142be4c1c9776a150cddb61a0d5bf3e95750b +"@babel/helper-validator-identifier@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-validator-identifier@npm:7.24.7" + checksum: 86875063f57361471b531dbc2ea10bbf5406e12b06d249b03827d361db4cad2388c6f00936bcd9dc86479f7e2c69ea21412c2228d4b3672588b754b70a449d4b languageName: node linkType: hard -"@babel/helper-validator-option@npm:^7.23.5": - version: 7.23.5 - resolution: "@babel/helper-validator-option@npm:7.23.5" - checksum: 537cde2330a8aede223552510e8a13e9c1c8798afee3757995a7d4acae564124fe2bf7e7c3d90d62d3657434a74340a274b3b3b1c6f17e9a2be1f48af29cb09e +"@babel/helper-validator-option@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/helper-validator-option@npm:7.24.8" + checksum: a52442dfa74be6719c0608fee3225bd0493c4057459f3014681ea1a4643cd38b68ff477fe867c4b356da7330d085f247f0724d300582fa4ab9a02efaf34d107c languageName: node linkType: hard -"@babel/helpers@npm:^7.24.4": - version: 7.24.4 - resolution: "@babel/helpers@npm:7.24.4" +"@babel/helpers@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/helpers@npm:7.24.8" dependencies: - "@babel/template": "npm:^7.24.0" - "@babel/traverse": "npm:^7.24.1" - "@babel/types": "npm:^7.24.0" - checksum: 54a9d0f86f2803fcc216cfa23b66b871ea0fa0a892af1c9a79075872c2437de71afbb150ed8216f30e00b19a0b9c5c9d5845173d170e1ebfbbf8887839b89dde + "@babel/template": "npm:^7.24.7" + "@babel/types": "npm:^7.24.8" + checksum: 61c08a2baa87382a87c7110e9b5574c782603e247b7e6267769ee0e8b7b54b70ff05f16466f05bb318622b7ac28e79b449edff565abf5adcb1adb1b0f42fee9c languageName: node linkType: hard -"@babel/highlight@npm:^7.10.4, @babel/highlight@npm:^7.24.2": - version: 7.24.2 - resolution: "@babel/highlight@npm:7.24.2" +"@babel/highlight@npm:^7.10.4, @babel/highlight@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/highlight@npm:7.24.7" dependencies: - "@babel/helper-validator-identifier": "npm:^7.22.20" + "@babel/helper-validator-identifier": "npm:^7.24.7" chalk: "npm:^2.4.2" js-tokens: "npm:^4.0.0" picocolors: "npm:^1.0.0" - checksum: 4555124235f34403bb28f55b1de58edf598491cc181c75f8afc8fe529903cb598cd52fe3bf2faab9bc1f45c299681ef0e44eea7a848bb85c500c5a4fe13f54f6 + checksum: 69b73f38cdd4f881b09b939a711e76646da34f4834f4ce141d7a49a6bb1926eab1c594148970a8aa9360398dff800f63aade4e81fafdd7c8d8a8489ea93bfec1 languageName: node linkType: hard -"@babel/parser@npm:^7.24.0, @babel/parser@npm:^7.24.1, @babel/parser@npm:^7.24.4, @babel/parser@npm:^7.6.0, @babel/parser@npm:^7.9.6": - version: 7.24.4 - resolution: "@babel/parser@npm:7.24.4" +"@babel/parser@npm:^7.24.7, @babel/parser@npm:^7.24.8, @babel/parser@npm:^7.6.0, @babel/parser@npm:^7.9.6": + version: 7.24.8 + resolution: "@babel/parser@npm:7.24.8" bin: parser: ./bin/babel-parser.js - checksum: 3742cc5068036287e6395269dce5a2735e6349cdc8d4b53297c75f98c580d7e1c8cb43235623999d151f2ef975d677dbc2c2357573a1855caa71c271bf3046c9 + checksum: e44b8327da46e8659bc9fb77f66e2dc4364dd66495fb17d046b96a77bf604f0446f1e9a89cf2f011d78fc3f5cdfbae2e9e0714708e1c985988335683b2e781ef languageName: node linkType: hard "@babel/runtime@npm:^7.6.3": - version: 7.24.4 - resolution: "@babel/runtime@npm:7.24.4" + version: 7.24.8 + resolution: "@babel/runtime@npm:7.24.8" dependencies: regenerator-runtime: "npm:^0.14.0" - checksum: 8ec8ce2c145bc7e31dd39ab66df124f357f65c11489aefacb30f431bae913b9aaa66aa5efe5321ea2bf8878af3fcee338c87e7599519a952e3a6f83aa1b03308 + checksum: e6f335e472a8a337379effc15815dd0eddf6a7d0c00b50deb4f9e9585819b45431d0ff3c2d3d0fa58c227a9b04dcc4a85e7245fb57493adb2863b5208c769cbd languageName: node linkType: hard -"@babel/template@npm:^7.22.15, @babel/template@npm:^7.24.0": - version: 7.24.0 - resolution: "@babel/template@npm:7.24.0" +"@babel/template@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/template@npm:7.24.7" dependencies: - "@babel/code-frame": "npm:^7.23.5" - "@babel/parser": "npm:^7.24.0" - "@babel/types": "npm:^7.24.0" - checksum: 8c538338c7de8fac8ada691a5a812bdcbd60bd4a4eb5adae2cc9ee19773e8fb1a724312a00af9e1ce49056ffd3c3475e7287b5668cf6360bfb3f8ac827a06ffe + "@babel/code-frame": "npm:^7.24.7" + "@babel/parser": "npm:^7.24.7" + "@babel/types": "npm:^7.24.7" + checksum: 5975d404ef51cf379515eb0f80b115981d0b9dff5539e53a47516644abb8c83d7559f5b083eb1d4977b20d8359ebb2f911ccd4f729143f8958fdc465f976d843 languageName: node linkType: hard -"@babel/traverse@npm:^7.24.1": - version: 7.24.1 - resolution: "@babel/traverse@npm:7.24.1" +"@babel/traverse@npm:^7.24.7, @babel/traverse@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/traverse@npm:7.24.8" dependencies: - "@babel/code-frame": "npm:^7.24.1" - "@babel/generator": "npm:^7.24.1" - "@babel/helper-environment-visitor": "npm:^7.22.20" - "@babel/helper-function-name": "npm:^7.23.0" - "@babel/helper-hoist-variables": "npm:^7.22.5" - "@babel/helper-split-export-declaration": "npm:^7.22.6" - "@babel/parser": "npm:^7.24.1" - "@babel/types": "npm:^7.24.0" + "@babel/code-frame": "npm:^7.24.7" + "@babel/generator": "npm:^7.24.8" + "@babel/helper-environment-visitor": "npm:^7.24.7" + "@babel/helper-function-name": "npm:^7.24.7" + "@babel/helper-hoist-variables": "npm:^7.24.7" + "@babel/helper-split-export-declaration": "npm:^7.24.7" + "@babel/parser": "npm:^7.24.8" + "@babel/types": "npm:^7.24.8" debug: "npm:^4.3.1" globals: "npm:^11.1.0" - checksum: b9b0173c286ef549e179f3725df3c4958069ad79fe5b9840adeb99692eb4a5a08db4e735c0f086aab52e7e08ec711cee9e7c06cb908d8035641d1382172308d3 + checksum: 47d8ecf8cfff58fe621fc4d8454b82c97c407816d8f9c435caa0c849ea7c357b91119a06f3c69f21a0228b5d06ac0b44f49d1f78cff032d6266317707f1fe615 languageName: node linkType: hard -"@babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.24.0, @babel/types@npm:^7.6.1, @babel/types@npm:^7.8.3, @babel/types@npm:^7.9.6": - version: 7.24.0 - resolution: "@babel/types@npm:7.24.0" +"@babel/types@npm:^7.24.7, @babel/types@npm:^7.24.8, @babel/types@npm:^7.24.9, @babel/types@npm:^7.6.1, @babel/types@npm:^7.8.3, @babel/types@npm:^7.9.6": + version: 7.24.9 + resolution: "@babel/types@npm:7.24.9" dependencies: - "@babel/helper-string-parser": "npm:^7.23.4" - "@babel/helper-validator-identifier": "npm:^7.22.20" + "@babel/helper-string-parser": "npm:^7.24.8" + "@babel/helper-validator-identifier": "npm:^7.24.7" to-fast-properties: "npm:^2.0.0" - checksum: a0b4875ce2e132f9daff0d5b27c7f4c4fcc97f2b084bdc5834e92c9d32592778489029e65d99d00c406da612d87b72d7a236c0afccaa1435c028d0c94c9b6da4 + checksum: 21873a08a124646824aa230de06af52149ab88206dca59849dcb3003990a6306ec2cdaa4147ec1127c0cfc5f133853cfc18f80d7f6337b6662a3c378ed565f15 languageName: node linkType: hard @@ -395,9 +391,9 @@ __metadata: linkType: hard "@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14": - version: 1.4.15 - resolution: "@jridgewell/sourcemap-codec@npm:1.4.15" - checksum: 89960ac087781b961ad918978975bcdf2051cd1741880469783c42de64239703eab9db5230d776d8e6a09d73bb5e4cb964e07d93ee6e2e7aea5a7d726e865c09 + version: 1.5.0 + resolution: "@jridgewell/sourcemap-codec@npm:1.5.0" + checksum: 4ed6123217569a1484419ac53f6ea0d9f3b57e5b57ab30d7c267bdb27792a27eb0e4b08e84a2680aa55cc2f2b411ffd6ec3db01c44fdc6dc43aca4b55f8374fd languageName: node linkType: hard @@ -431,11 +427,11 @@ __metadata: linkType: hard "@noble/curves@npm:^1.3.0": - version: 1.4.0 - resolution: "@noble/curves@npm:1.4.0" + version: 1.4.2 + resolution: "@noble/curves@npm:1.4.2" dependencies: "@noble/hashes": "npm:1.4.0" - checksum: b21b30a36ff02bfcc0f5e6163d245cdbaf7f640511fff97ccf83fc207ee79cfd91584b4d97977374de04cb118a55eb63a7964c82596a64162bbc42bc685ae6d9 + checksum: f433a2e8811ae345109388eadfa18ef2b0004c1f79417553241db4f0ad0d59550be6298a4f43d989c627e9f7551ffae6e402a4edf0173981e6da95fc7cab5123 languageName: node linkType: hard @@ -460,47 +456,11 @@ __metadata: linkType: hard "@npmcli/fs@npm:^3.1.0": - version: 3.1.0 - resolution: "@npmcli/fs@npm:3.1.0" + version: 3.1.1 + resolution: "@npmcli/fs@npm:3.1.1" dependencies: semver: "npm:^7.3.5" - checksum: f3a7ab3a31de65e42aeb6ed03ed035ef123d2de7af4deb9d4a003d27acc8618b57d9fb9d259fe6c28ca538032a028f37337264388ba27d26d37fff7dde22476e - languageName: node - linkType: hard - -"@opencensus/core@npm:0.0.9": - version: 0.0.9 - resolution: "@opencensus/core@npm:0.0.9" - dependencies: - continuation-local-storage: "npm:^3.2.1" - log-driver: "npm:^1.2.7" - semver: "npm:^5.5.0" - shimmer: "npm:^1.2.0" - uuid: "npm:^3.2.1" - checksum: cd9dd6351edde5287e10260d97a1d324af2c8c226328bc95d35a6a4b4072a50080759617223b63428e19a31f50c32710838d32ba8d576fcfc2a76e0056a9b7da - languageName: node - linkType: hard - -"@opencensus/core@npm:^0.0.8": - version: 0.0.8 - resolution: "@opencensus/core@npm:0.0.8" - dependencies: - continuation-local-storage: "npm:^3.2.1" - log-driver: "npm:^1.2.7" - semver: "npm:^5.5.0" - shimmer: "npm:^1.2.0" - uuid: "npm:^3.2.1" - checksum: 079869d161800e68f7a277591567be033e29cf88363134986bee46dce84fab389cfe29ffd814881109174890d44275aae5da7a0ab0788166b1c174d73211c42a - languageName: node - linkType: hard - -"@opencensus/propagation-b3@npm:0.0.8": - version: 0.0.8 - resolution: "@opencensus/propagation-b3@npm:0.0.8" - dependencies: - "@opencensus/core": "npm:^0.0.8" - uuid: "npm:^3.2.1" - checksum: 00eac0f87e3f93633da587b0c95dfe3d54f96e91751c0e4f1d3c8b0892a7ef04f84782a88ab53d662344b30b0da60a283fdd5ccd20faa48fb02375871afd1051 + checksum: 1e0e04087049b24b38bc0b30d87a9388ee3ca1d3fdfc347c2f77d84fcfe6a51f250bc57ba2c1f614d7e4285c6c62bf8c769bc19aa0949ea39e5b043ee023b0bd languageName: node linkType: hard @@ -566,8 +526,8 @@ __metadata: linkType: hard "@pm2/agent@npm:~2.0.0": - version: 2.0.3 - resolution: "@pm2/agent@npm:2.0.3" + version: 2.0.4 + resolution: "@pm2/agent@npm:2.0.4" dependencies: async: "npm:~3.2.0" chalk: "npm:~3.0.0" @@ -581,17 +541,15 @@ __metadata: pm2-axon-rpc: "npm:~0.7.0" proxy-agent: "npm:~6.3.0" semver: "npm:~7.5.0" - ws: "npm:~7.4.0" - checksum: 0642f56dc7f36d17b3c45a71ac39681e1e10d99254d0f64798555cfe8aa6837106bc1d9595777c47b01ef85006359477880bada02a196db964c78dce25cbaf07 + ws: "npm:~7.5.10" + checksum: e7d5a48637ce78850bdb4c60dc0e2c763c0ce0eace716f16c6f50a7431b616acea5ae0901d4cf31f281a5bac4d59de92729ea52eee4a6b23178155294ae40532 languageName: node linkType: hard -"@pm2/io@npm:~5.0.0": - version: 5.0.2 - resolution: "@pm2/io@npm:5.0.2" +"@pm2/io@npm:~6.0.1": + version: 6.0.1 + resolution: "@pm2/io@npm:6.0.1" dependencies: - "@opencensus/core": "npm:0.0.9" - "@opencensus/propagation-b3": "npm:0.0.8" async: "npm:~2.6.1" debug: "npm:~4.3.1" eventemitter2: "npm:^6.3.1" @@ -600,7 +558,7 @@ __metadata: shimmer: "npm:^1.2.0" signal-exit: "npm:^3.0.3" tslib: "npm:1.9.3" - checksum: d589271efe8abe2d25c39501f37a9870073f6d308c2ed1bae1db38528d3a2a6fdad2b0bf464a808d6959472bc1f6c28746792084982ccf771d858e68703b36a6 + checksum: b0763b7204bb4609d3b09411c5d2b7184de0a93747f033dfbbb07e7fd158c7ffe0cfd15e5728a131b93d38c21f9e34f361e2bc2bdddf1bd48638e401e6c98587 languageName: node linkType: hard @@ -1101,9 +1059,9 @@ __metadata: linkType: hard "@scure/base@npm:^1.1.1, @scure/base@npm:^1.1.5": - version: 1.1.6 - resolution: "@scure/base@npm:1.1.6" - checksum: 814fd1cce24f1e152751fabca2853d26aaa96ff8a9349c43d9aebc3b3d8ca88dd902966e1c289590a37f35d4c4436c6aedb1b386924b2909072045af4c3e9fe4 + version: 1.1.7 + resolution: "@scure/base@npm:1.1.7" + checksum: fc50ffaab36cb46ff9fa4dc5052a06089ab6a6707f63d596bb34aaaec76173c9a564ac312a0b981b5e7a5349d60097b8878673c75d6cbfc4da7012b63a82099b languageName: node linkType: hard @@ -1152,9 +1110,9 @@ __metadata: linkType: hard "@stellar/js-xdr@npm:^3.1.1": - version: 3.1.1 - resolution: "@stellar/js-xdr@npm:3.1.1" - checksum: 3bc8ee3f1611b55938ef0249b7a90b3d689177d45b4c8c24c5562b8fe32372148f7544a0aab67776f4c7b95aef8cb0f97606b86ec800bb087302ff404cb8ccbc + version: 3.1.2 + resolution: "@stellar/js-xdr@npm:3.1.2" + checksum: 96b5c52088bb2f2cc11a04ee1766ceb9431bdb0195058b9bc8d60cd1459772c2be6a9aa1bf69ba8b7589cfaf71beef96890e60d7fd7de0332c11ae1917f95440 languageName: node linkType: hard @@ -1184,9 +1142,9 @@ __metadata: linkType: hard "@substrate/connect-known-chains@npm:^1.1.1": - version: 1.1.4 - resolution: "@substrate/connect-known-chains@npm:1.1.4" - checksum: 17fdce09bf2ba042371910a5e1c701d7db7e40b7c021eb67f36ed2d28175b8846e01eb2438be03827e2cd2654d35be5a1a248c36e9588ccdbe4545da0bee5047 + version: 1.1.11 + resolution: "@substrate/connect-known-chains@npm:1.1.11" + checksum: b113cf7c1c562e6c726738ae41065038c19ff77f96e86280e55d91bcb197e8412f2de855cf9bdd12e5dbe9a130d10022959f783308647d360a9aa494ed8d2287 languageName: node linkType: hard @@ -1220,9 +1178,9 @@ __metadata: linkType: hard "@substrate/ss58-registry@npm:^1.44.0": - version: 1.47.0 - resolution: "@substrate/ss58-registry@npm:1.47.0" - checksum: f78edac6ad88a3063bc0474b2ae2394b5af60cf61f474f4c4ad171346f8debba9fc8b047f0046ccb1abddeb7c13377a5fcf2046c0496a26a1a192cc7a4224ff4 + version: 1.49.0 + resolution: "@substrate/ss58-registry@npm:1.49.0" + checksum: 6214c8f8586aefbb67b70bd2f02a9d133cfb15f360d691f1164c192036b59d1e282495e884b7da80a32e54eed1bd2bcc421f571e548a29410a5a50be7b2daa10 languageName: node linkType: hard @@ -1299,11 +1257,11 @@ __metadata: linkType: hard "@types/node@npm:*": - version: 20.12.7 - resolution: "@types/node@npm:20.12.7" + version: 20.14.12 + resolution: "@types/node@npm:20.14.12" dependencies: undici-types: "npm:~5.26.4" - checksum: b4a28a3b593a9bdca5650880b6a9acef46911d58cf7cfa57268f048e9a7157a7c3196421b96cea576850ddb732e3b54bc982c8eb5e1e5ef0635d4424c2fce801 + checksum: 9205bf46ef6a99d99cdde9efeb8218cd15803cc407249c2336557cd630b006380dad68c03ee574934414639f8e450044f45530c92788a8e82078bae45ee40f93 languageName: node linkType: hard @@ -1321,13 +1279,6 @@ __metadata: languageName: node linkType: hard -"abbrev@npm:1": - version: 1.1.1 - resolution: "abbrev@npm:1.1.1" - checksum: 2d882941183c66aa665118bafdab82b7a177e9add5eb2776c33e960a4f3c89cff88a1b38aba13a456de01d0dd9d66a8bea7c903268b21ea91dd1097e1e2e8243 - languageName: node - linkType: hard - "abbrev@npm:^2.0.0": version: 2.0.0 resolution: "abbrev@npm:2.0.0" @@ -1422,14 +1373,14 @@ __metadata: linkType: hard "ajv@npm:^8.0.1": - version: 8.12.0 - resolution: "ajv@npm:8.12.0" + version: 8.17.1 + resolution: "ajv@npm:8.17.1" dependencies: - fast-deep-equal: "npm:^3.1.1" + fast-deep-equal: "npm:^3.1.3" + fast-uri: "npm:^3.0.1" json-schema-traverse: "npm:^1.0.0" require-from-string: "npm:^2.0.2" - uri-js: "npm:^4.2.2" - checksum: b406f3b79b5756ac53bfe2c20852471b08e122bc1ee4cde08ae4d6a800574d9cd78d60c81c69c63ff81e4da7cd0b638fafbb2303ae580d49cf1600b9059efb85 + checksum: ee3c62162c953e91986c838f004132b6a253d700f1e51253b99791e2dbfdb39161bc950ebdc2f156f8568035bb5ed8be7bd78289cd9ecbf3381fe8f5b82e3f33 languageName: node linkType: hard @@ -1594,6 +1545,13 @@ __metadata: languageName: node linkType: hard +"argparse@npm:^2.0.1": + version: 2.0.1 + resolution: "argparse@npm:2.0.1" + checksum: 18640244e641a417ec75a9bd38b0b2b6b95af5199aa241b131d4b2fb206f334d7ecc600bd194861610a5579084978bfcbb02baa399dbe442d56d0ae5e60dbaef + languageName: node + linkType: hard + "array-buffer-byte-length@npm:^1.0.1": version: 1.0.1 resolution: "array-buffer-byte-length@npm:1.0.1" @@ -1753,9 +1711,9 @@ __metadata: linkType: hard "assert-never@npm:^1.2.1": - version: 1.2.1 - resolution: "assert-never@npm:1.2.1" - checksum: ea4f1756d90f55254c4dc7a20d6c5d5bc169160562aefe3d8756b598c10e695daf568f21b6d6b12245d7f3782d3ff83ef6a01ab75d487adfc6909470a813bf8c + version: 1.3.0 + resolution: "assert-never@npm:1.3.0" + checksum: 7ba7b06433bb4155ed0e7e6be4c65dbf4b0221441beb761d6c418d5ac9e3bdd1f6db9c5eeffb895eaf31a388e21f23b2a4f99af3194f54c2ea0e93edab8a3d8c languageName: node linkType: hard @@ -1789,16 +1747,6 @@ __metadata: languageName: node linkType: hard -"async-listener@npm:^0.6.0": - version: 0.6.10 - resolution: "async-listener@npm:0.6.10" - dependencies: - semver: "npm:^5.3.0" - shimmer: "npm:^1.1.0" - checksum: dcf3d9320215f4f4594de03f46587743ebd742ec4993ff49649b948f62c967be62114aaeea3bc2ddae2b451a8452b9989ae565a84eda453f9b9d56d929a8dc85 - languageName: node - linkType: hard - "async@npm:^2.6.3, async@npm:~2.6.1": version: 2.6.4 resolution: "async@npm:2.6.4" @@ -1846,20 +1794,20 @@ __metadata: linkType: hard "aws4@npm:^1.8.0": - version: 1.12.0 - resolution: "aws4@npm:1.12.0" - checksum: 2b8455fe1eee87f0e7d5f32e81e7fec74dce060c72d03f528c8c631fa74209cef53aab6fede182ea17d0c9520cb1e5e3023c5fedb4f1139ae9f067fc720869a5 + version: 1.13.0 + resolution: "aws4@npm:1.13.0" + checksum: a73a43f88c5d915e564d102a6b181a62afd7991f25e661b440540fdef102cbccce7cfa7da8b82ea1c34645e672ac617aecbd9f4f1e91e3f9e99de4d1d7a2cef9 languageName: node linkType: hard "axios@npm:^1.6.8": - version: 1.6.8 - resolution: "axios@npm:1.6.8" + version: 1.7.2 + resolution: "axios@npm:1.7.2" dependencies: follow-redirects: "npm:^1.15.6" form-data: "npm:^4.0.0" proxy-from-env: "npm:^1.1.0" - checksum: 3f9a79eaf1d159544fca9576261ff867cbbff64ed30017848e4210e49f3b01e97cf416390150e6fdf6633f336cd43dc1151f890bbd09c3c01ad60bb0891eee63 + checksum: 6ae80dda9736bb4762ce717f1a26ff997d94672d3a5799ad9941c24d4fb019c1dff45be8272f08d1975d7950bac281f3ba24aff5ecd49ef5a04d872ec428782f languageName: node linkType: hard @@ -2071,11 +2019,11 @@ __metadata: linkType: hard "braces@npm:~3.0.2": - version: 3.0.2 - resolution: "braces@npm:3.0.2" + version: 3.0.3 + resolution: "braces@npm:3.0.3" dependencies: - fill-range: "npm:^7.0.1" - checksum: 966b1fb48d193b9d155f810e5efd1790962f2c4e0829f8440b8ad236ba009222c501f70185ef732fef17a4c490bb33a03b90dab0631feafbdf447da91e8165b1 + fill-range: "npm:^7.1.1" + checksum: fad11a0d4697a27162840b02b1fad249c1683cbc510cd5bf1a471f2f8085c046d41094308c577a50a03a579dd99d5a6b3724c4b5e8b14df2c4443844cfcda2c6 languageName: node linkType: hard @@ -2086,17 +2034,17 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.22.2": - version: 4.23.0 - resolution: "browserslist@npm:4.23.0" +"browserslist@npm:^4.23.1": + version: 4.23.2 + resolution: "browserslist@npm:4.23.2" dependencies: - caniuse-lite: "npm:^1.0.30001587" - electron-to-chromium: "npm:^1.4.668" + caniuse-lite: "npm:^1.0.30001640" + electron-to-chromium: "npm:^1.4.820" node-releases: "npm:^2.0.14" - update-browserslist-db: "npm:^1.0.13" + update-browserslist-db: "npm:^1.1.0" bin: browserslist: cli.js - checksum: 496c3862df74565dd942b4ae65f502c575cbeba1fa4a3894dad7aa3b16130dc3033bc502d8848147f7b625154a284708253d9598bcdbef5a1e34cf11dc7bad8e + checksum: 326a98b1c39bcc9a99b197f15790dc28e122b1aead3257c837421899377ac96239123f26868698085b3d9be916d72540602738e1f857e86a387e810af3fda6e5 languageName: node linkType: hard @@ -2146,8 +2094,8 @@ __metadata: linkType: hard "cacache@npm:^18.0.0": - version: 18.0.2 - resolution: "cacache@npm:18.0.2" + version: 18.0.4 + resolution: "cacache@npm:18.0.4" dependencies: "@npmcli/fs": "npm:^3.1.0" fs-minipass: "npm:^3.0.0" @@ -2161,7 +2109,7 @@ __metadata: ssri: "npm:^10.0.0" tar: "npm:^6.1.11" unique-filename: "npm:^3.0.0" - checksum: 5ca58464f785d4d64ac2019fcad95451c8c89bea25949f63acd8987fcc3493eaef1beccc0fa39e673506d879d3fc1ab420760f8a14f8ddf46ea2d121805a5e96 + checksum: ca2f7b2d3003f84d362da9580b5561058ccaecd46cba661cbcff0375c90734b610520d46b472a339fd032d91597ad6ed12dde8af81571197f3c9772b5d35b104 languageName: node linkType: hard @@ -2254,10 +2202,10 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.30001587": - version: 1.0.30001612 - resolution: "caniuse-lite@npm:1.0.30001612" - checksum: 8fb95102aade9147694541a9e576ec16d8d455f37e1456f497403af45f1ddd24465a62057d619d57c052e9634e090e5115e383ab066f8f9f9b87d14f738f81df +"caniuse-lite@npm:^1.0.30001640": + version: 1.0.30001643 + resolution: "caniuse-lite@npm:1.0.30001643" + checksum: dddbda29fa24fbc435873309c71070461cbfc915d9bce3216180524c20c5637b2bee1a14b45972e9ac19e1fdf63fba3f63608b9e7d68de32f5ee1953c8c69e05 languageName: node linkType: hard @@ -2279,13 +2227,13 @@ __metadata: linkType: hard "chai-as-promised@npm:^7.1.1": - version: 7.1.1 - resolution: "chai-as-promised@npm:7.1.1" + version: 7.1.2 + resolution: "chai-as-promised@npm:7.1.2" dependencies: check-error: "npm:^1.0.2" peerDependencies: - chai: ">= 2.1.2 < 5" - checksum: 5d9ecab37b313047f5ea25d00b1cb6e7f2710c6e2f57d91aed7cfed5008d995cb65ea723af4e5d782bafd9a6eff5a4267af53dfe7212dc10dd1d92b9127bc531 + chai: ">= 2.1.2 < 6" + checksum: be372540dad92ef85cde3954bc0e9b0b33e4e6454f3740b17bfb16e36eda638911619089c05a4e4f2bf6722563bf893bb78c2af59b318c23abb2199e5c20ca1f languageName: node linkType: hard @@ -2659,16 +2607,6 @@ __metadata: languageName: node linkType: hard -"continuation-local-storage@npm:^3.2.1": - version: 3.2.1 - resolution: "continuation-local-storage@npm:3.2.1" - dependencies: - async-listener: "npm:^0.6.0" - emitter-listener: "npm:^1.1.1" - checksum: 3b86d158012f33144a82c669453553c147ec14017ed078411f88a8ffaa5b4c7b6648d9fd814ab03dd024df09ead95afd4fcc0142242fe23557ec34a1c7b1d2e9 - languageName: node - linkType: hard - "convert-source-map@npm:^1.7.0": version: 1.9.0 resolution: "convert-source-map@npm:1.9.0" @@ -2917,9 +2855,9 @@ __metadata: linkType: hard "dayjs@npm:^1.8.16, dayjs@npm:~1.11.5": - version: 1.11.10 - resolution: "dayjs@npm:1.11.10" - checksum: 27e8f5bc01c0a76f36c656e62ab7f08c2e7b040b09e613cd4844abf03fb258e0350f0a83b02c887b84d771c1f11e092deda0beef8c6df2a1afbc3f6c1fade279 + version: 1.11.12 + resolution: "dayjs@npm:1.11.12" + checksum: 8ee7c1e14961fd08d40b788d0c0e930dc6288b3d32911bb911b2fb31bb703c262788164fbe678ee9e50e2a35268d667b8c8ba43fd1806771c1f404c300a2b428 languageName: node linkType: hard @@ -2931,14 +2869,14 @@ __metadata: linkType: hard "debug@npm:*, debug@npm:4, debug@npm:^4.0.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.4, debug@npm:~4.3.1": - version: 4.3.4 - resolution: "debug@npm:4.3.4" + version: 4.3.5 + resolution: "debug@npm:4.3.5" dependencies: ms: "npm:2.1.2" peerDependenciesMeta: supports-color: optional: true - checksum: 0073c3bcbd9cb7d71dd5f6b55be8701af42df3e56e911186dfa46fac3a5b9eb7ce7f377dd1d3be6db8977221f8eb333d945216f645cf56f6b688cd484837d255 + checksum: cb6eab424c410e07813ca1392888589972ce9a32b8829c6508f5e1f25f3c3e70a76731610ae55b4bbe58d1a2fffa1424b30e97fa8d394e49cd2656a9643aedd2 languageName: node linkType: hard @@ -2996,11 +2934,11 @@ __metadata: linkType: hard "deep-eql@npm:^4.1.3": - version: 4.1.3 - resolution: "deep-eql@npm:4.1.3" + version: 4.1.4 + resolution: "deep-eql@npm:4.1.4" dependencies: type-detect: "npm:^4.0.0" - checksum: 12ce93ae63de187e77b076d3d51bfc28b11f98910a22c18714cce112791195e86a94f97788180994614b14562a86c9763f67c69f785e4586f806b5df39bf9301 + checksum: f04f4d581f044a824a6322fe4f68fbee4d6780e93fc710cd9852cbc82bfc7010df00f0e05894b848abbe14dc3a25acac44f424e181ae64d12f2ab9d0a875a5ef languageName: node linkType: hard @@ -3038,7 +2976,7 @@ __metadata: languageName: node linkType: hard -"define-properties@npm:^1.1.2, define-properties@npm:^1.1.3, define-properties@npm:^1.2.0, define-properties@npm:^1.2.1": +"define-properties@npm:^1.1.2, define-properties@npm:^1.2.0, define-properties@npm:^1.2.1": version: 1.2.1 resolution: "define-properties@npm:1.2.1" dependencies: @@ -3282,10 +3220,10 @@ __metadata: languageName: node linkType: hard -"electron-to-chromium@npm:^1.4.668": - version: 1.4.748 - resolution: "electron-to-chromium@npm:1.4.748" - checksum: c1a5664f151d49209df43c8a33cf02f7004591ac5c9b4d685e5dfe3a0470843358f7578de4c21e26108a7bd7e9629a20ac92c7ccc76f4cfbc535af5afaf097aa +"electron-to-chromium@npm:^1.4.820": + version: 1.5.0 + resolution: "electron-to-chromium@npm:1.5.0" + checksum: 49f5df3bb7307fef536bad0d98ad297d34a19f129c9b2b7c1a585e9b2120724cafcd41c9848e91882ebe069575a5faee0ab5a03c935f0a8b1b57398468352115 languageName: node linkType: hard @@ -3308,15 +3246,6 @@ __metadata: languageName: node linkType: hard -"emitter-listener@npm:^1.1.1": - version: 1.1.2 - resolution: "emitter-listener@npm:1.1.2" - dependencies: - shimmer: "npm:^1.2.0" - checksum: 697f53c30841eb380240b27b385f55596d66ff2d8c479ca3af2ad448cbbeb930d87f7c70105be5467a1424bdd0dfb161173238df413a2c79d8263b9140f917be - languageName: node - linkType: hard - "emoji-regex@npm:^7.0.1": version: 7.0.3 resolution: "emoji-regex@npm:7.0.3" @@ -3557,7 +3486,7 @@ __metadata: languageName: node linkType: hard -"escalade@npm:^3.1.1": +"escalade@npm:^3.1.2": version: 3.1.2 resolution: "escalade@npm:3.1.2" checksum: a1e07fea2f15663c30e40b9193d658397846ffe28ce0a3e4da0d8e485fedfeca228ab846aee101a05015829adf39f9934ff45b2a3fca47bed37a29646bd05cd3 @@ -3783,11 +3712,11 @@ __metadata: linkType: hard "esquery@npm:^1.4.0": - version: 1.5.0 - resolution: "esquery@npm:1.5.0" + version: 1.6.0 + resolution: "esquery@npm:1.6.0" dependencies: estraverse: "npm:^5.1.0" - checksum: e65fcdfc1e0ff5effbf50fb4f31ea20143ae5df92bb2e4953653d8d40aa4bc148e0d06117a592ce4ea53eeab1dafdfded7ea7e22a5be87e82d73757329a1b01d + checksum: c587fb8ec9ed83f2b1bc97cf2f6854cc30bf784a79d62ba08c6e358bf22280d69aee12827521cf38e69ae9761d23fb7fde593ce315610f85655c139d99b05e5a languageName: node linkType: hard @@ -4010,6 +3939,13 @@ __metadata: languageName: node linkType: hard +"fast-uri@npm:^3.0.1": + version: 3.0.1 + resolution: "fast-uri@npm:3.0.1" + checksum: e8ee4712270de0d29eb0fbf41ffad0ac80952e8797be760e8bb62c4707f08f50a86fe2d7829681ca133c07d6eb4b4a75389a5fc36674c5b254a3ac0891a68fc7 + languageName: node + linkType: hard + "fclone@npm:1.0.11, fclone@npm:~1.0.11": version: 1.0.11 resolution: "fclone@npm:1.0.11" @@ -4050,12 +3986,12 @@ __metadata: languageName: node linkType: hard -"fill-range@npm:^7.0.1": - version: 7.0.1 - resolution: "fill-range@npm:7.0.1" +"fill-range@npm:^7.1.1": + version: 7.1.1 + resolution: "fill-range@npm:7.1.1" dependencies: to-regex-range: "npm:^5.0.1" - checksum: e260f7592fd196b4421504d3597cc76f4a1ca7a9488260d533b611fc3cefd61e9a9be1417cb82d3b01ad9f9c0ff2dbf258e1026d2445e26b0cf5148ff4250429 + checksum: a7095cb39e5bc32fada2aa7c7249d3f6b01bd1ce461a61b0adabacccabd9198500c6fb1f68a7c851a657e273fce2233ba869638897f3d7ed2e87a2d89b4436ea languageName: node linkType: hard @@ -4179,12 +4115,12 @@ __metadata: linkType: hard "foreground-child@npm:^3.1.0": - version: 3.1.1 - resolution: "foreground-child@npm:3.1.1" + version: 3.2.1 + resolution: "foreground-child@npm:3.2.1" dependencies: cross-spawn: "npm:^7.0.0" signal-exit: "npm:^4.0.1" - checksum: 087edd44857d258c4f73ad84cb8df980826569656f2550c341b27adf5335354393eec24ea2fabd43a253233fb27cee177ebe46bd0b7ea129c77e87cb1e9936fb + checksum: 77b33b3c438a499201727ca84de39a66350ccd54a8805df712773e963cefb5c4632dbc4386109e97a0df8fb1585aee95fa35acb07587e3e04cfacabfc0ae15dc languageName: node linkType: hard @@ -4495,21 +4431,22 @@ __metadata: linkType: hard "glob@npm:^10.2.2, glob@npm:^10.3.10": - version: 10.3.12 - resolution: "glob@npm:10.3.12" + version: 10.4.5 + resolution: "glob@npm:10.4.5" dependencies: foreground-child: "npm:^3.1.0" - jackspeak: "npm:^2.3.6" - minimatch: "npm:^9.0.1" - minipass: "npm:^7.0.4" - path-scurry: "npm:^1.10.2" + jackspeak: "npm:^3.1.2" + minimatch: "npm:^9.0.4" + minipass: "npm:^7.1.2" + package-json-from-dist: "npm:^1.0.0" + path-scurry: "npm:^1.11.1" bin: glob: dist/esm/bin.mjs - checksum: 9e8186abc22dc824b5dd86cefd8e6b5621a72d1be7f68bacc0fd681e8c162ec5546660a6ec0553d6a74757a585e655956c7f8f1a6d24570e8d865c307323d178 + checksum: 698dfe11828b7efd0514cd11e573eaed26b2dff611f0400907281ce3eab0c1e56143ef9b35adc7c77ecc71fba74717b510c7c223d34ca8a98ec81777b293d4ac languageName: node linkType: hard -"glob@npm:^7.0.5, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6": +"glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6": version: 7.2.3 resolution: "glob@npm:7.2.3" dependencies: @@ -4540,11 +4477,12 @@ __metadata: linkType: hard "globalthis@npm:^1.0.3": - version: 1.0.3 - resolution: "globalthis@npm:1.0.3" + version: 1.0.4 + resolution: "globalthis@npm:1.0.4" dependencies: - define-properties: "npm:^1.1.3" - checksum: 45ae2f3b40a186600d0368f2a880ae257e8278b4c7704f0417d6024105ad7f7a393661c5c2fa1334669cd485ea44bc883a08fdd4516df2428aec40c99f52aa89 + define-properties: "npm:^1.2.1" + gopd: "npm:^1.0.1" + checksum: 1f1fd078fb2f7296306ef9dd51019491044ccf17a59ed49d375b576ca108ff37e47f3d29aead7add40763574a992f16a5367dd1e2173b8634ef18556ab719ac4 languageName: node linkType: hard @@ -4810,13 +4748,13 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.2": - version: 7.0.4 - resolution: "https-proxy-agent@npm:7.0.4" +"https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.2, https-proxy-agent@npm:^7.0.5": + version: 7.0.5 + resolution: "https-proxy-agent@npm:7.0.5" dependencies: agent-base: "npm:^7.0.2" debug: "npm:4" - checksum: 405fe582bba461bfe5c7e2f8d752b384036854488b828ae6df6a587c654299cbb2c50df38c4b6ab303502c3c5e029a793fbaac965d1e86ee0be03faceb554d63 + checksum: 6679d46159ab3f9a5509ee80c3a3fc83fba3a920a5e18d32176c3327852c3c00ad640c0c4210a8fd70ea3c4a6d3a1b375bf01942516e7df80e2646bdc77658ab languageName: node linkType: hard @@ -5080,11 +5018,11 @@ __metadata: linkType: hard "is-core-module@npm:^2.13.0, is-core-module@npm:^2.13.1": - version: 2.13.1 - resolution: "is-core-module@npm:2.13.1" + version: 2.15.0 + resolution: "is-core-module@npm:2.15.0" dependencies: - hasown: "npm:^2.0.0" - checksum: d53bd0cc24b0a0351fb4b206ee3908f71b9bbf1c47e9c9e14e5f06d292af1663704d2abd7e67700d6487b2b7864e0d0f6f10a1edf1892864bdffcb197d1845a2 + hasown: "npm:^2.0.2" + checksum: 70e962543e5d3a97c07cb29144a86792d545a21f28e67da5401d85878a0193d46fbab8d97bc3ca680e2778705dca66e7b6ca840c493497a27ca0e8c5f3ac3d1d languageName: node linkType: hard @@ -5427,16 +5365,16 @@ __metadata: languageName: node linkType: hard -"jackspeak@npm:^2.3.6": - version: 2.3.6 - resolution: "jackspeak@npm:2.3.6" +"jackspeak@npm:^3.1.2": + version: 3.4.3 + resolution: "jackspeak@npm:3.4.3" dependencies: "@isaacs/cliui": "npm:^8.0.2" "@pkgjs/parseargs": "npm:^0.11.0" dependenciesMeta: "@pkgjs/parseargs": optional: true - checksum: 6e6490d676af8c94a7b5b29b8fd5629f21346911ebe2e32931c2a54210134408171c24cee1a109df2ec19894ad04a429402a8438cbf5cc2794585d35428ace76 + checksum: 96f8786eaab98e4bf5b2a5d6d9588ea46c4d06bbc4f2eb861fdd7b6b182b16f71d8a70e79820f335d52653b16d4843b29dd9cdcf38ae80406756db9199497cf3 languageName: node linkType: hard @@ -5502,6 +5440,17 @@ __metadata: languageName: node linkType: hard +"js-yaml@npm:~4.1.0": + version: 4.1.0 + resolution: "js-yaml@npm:4.1.0" + dependencies: + argparse: "npm:^2.0.1" + bin: + js-yaml: bin/js-yaml.js + checksum: c138a34a3fd0d08ebaf71273ad4465569a483b8a639e0b118ff65698d257c2791d3199e3f303631f2cb98213fa7b5f5d6a4621fd0fff819421b990d30d967140 + languageName: node + linkType: hard + "jsbn@npm:1.1.0": version: 1.1.0 resolution: "jsbn@npm:1.1.0" @@ -6014,9 +5963,9 @@ __metadata: languageName: node linkType: hard -"logform@npm:^2.3.2, logform@npm:^2.4.0": - version: 2.6.0 - resolution: "logform@npm:2.6.0" +"logform@npm:^2.6.0, logform@npm:^2.6.1": + version: 2.6.1 + resolution: "logform@npm:2.6.1" dependencies: "@colors/colors": "npm:1.6.0" "@types/triple-beam": "npm:^1.3.2" @@ -6024,7 +5973,7 @@ __metadata: ms: "npm:^2.1.1" safe-stable-stringify: "npm:^2.3.1" triple-beam: "npm:^1.3.0" - checksum: 92de5696a529a7ccf4359fe65a21fce2398ba20c4b4e5769cba187b8fde01d590a22d3c83f797d31b436f49770fb1b2f28646e7c881d30b8d1f4080a05ae7006 + checksum: e67f414787fbfe1e6a997f4c84300c7e06bee3d0bd579778af667e24b36db3ea200ed195d41b61311ff738dab7faabc615a07b174b22fe69e0b2f39e985be64b languageName: node linkType: hard @@ -6071,9 +6020,9 @@ __metadata: linkType: hard "lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": - version: 10.2.0 - resolution: "lru-cache@npm:10.2.0" - checksum: 502ec42c3309c0eae1ce41afca471f831c278566d45a5273a0c51102dee31e0e250a62fa9029c3370988df33a14188a38e682c16143b794de78668de3643e302 + version: 10.4.3 + resolution: "lru-cache@npm:10.4.3" + checksum: e6e90267360476720fa8e83cc168aa2bf0311f3f2eea20a6ba78b90a885ae72071d9db132f40fda4129c803e7dcec3a6b6a6fbb44ca90b081630b810b5d6a41a languageName: node linkType: hard @@ -6121,8 +6070,8 @@ __metadata: linkType: hard "make-fetch-happen@npm:^13.0.0": - version: 13.0.0 - resolution: "make-fetch-happen@npm:13.0.0" + version: 13.0.1 + resolution: "make-fetch-happen@npm:13.0.1" dependencies: "@npmcli/agent": "npm:^2.0.0" cacache: "npm:^18.0.0" @@ -6133,9 +6082,10 @@ __metadata: minipass-flush: "npm:^1.0.5" minipass-pipeline: "npm:^1.2.4" negotiator: "npm:^0.6.3" + proc-log: "npm:^4.2.0" promise-retry: "npm:^2.0.1" ssri: "npm:^10.0.0" - checksum: ded5a91a02b76381b06a4ec4d5c1d23ebbde15d402b3c3e4533b371dac7e2f7ca071ae71ae6dae72aa261182557b7b1b3fd3a705b39252dc17f74fa509d3e76f + checksum: 11bae5ad6ac59b654dbd854f30782f9de052186c429dfce308eda42374528185a100ee40ac9ffdc36a2b6c821ecaba43913e4730a12f06f15e895ea9cb23fa59 languageName: node linkType: hard @@ -6292,13 +6242,20 @@ __metadata: languageName: node linkType: hard -"mime-db@npm:1.52.0, mime-db@npm:>= 1.43.0 < 2": +"mime-db@npm:1.52.0": version: 1.52.0 resolution: "mime-db@npm:1.52.0" checksum: 54bb60bf39e6f8689f6622784e668a3d7f8bed6b0d886f5c3c446cb3284be28b30bf707ed05d0fe44a036f8469976b2629bbea182684977b084de9da274694d7 languageName: node linkType: hard +"mime-db@npm:>= 1.43.0 < 2": + version: 1.53.0 + resolution: "mime-db@npm:1.53.0" + checksum: 82409c568a20254cc67a763a25e581d2213e1ef5d070a0af805239634f8a655f5d8a15138200f5f81c5b06fc6623d27f6168c612d447642d59e37eb7f20f7412 + languageName: node + linkType: hard + "mime-types@npm:^2.1.12, mime-types@npm:~2.1.19, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" @@ -6353,12 +6310,12 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^9.0.1": - version: 9.0.4 - resolution: "minimatch@npm:9.0.4" +"minimatch@npm:^9.0.4": + version: 9.0.5 + resolution: "minimatch@npm:9.0.5" dependencies: brace-expansion: "npm:^2.0.1" - checksum: 4cdc18d112b164084513e890d6323370db14c22249d536ad1854539577a895e690a27513dc346392f61a4a50afbbd8abc88f3f25558bfbbbb862cd56508b20f5 + checksum: dd6a8927b063aca6d910b119e1f2df6d2ce7d36eab91de83167dd136bb85e1ebff97b0d3de1cb08bd1f7e018ca170b4962479fefab5b2a69e2ae12cb2edc8348 languageName: node linkType: hard @@ -6389,8 +6346,8 @@ __metadata: linkType: hard "minipass-fetch@npm:^3.0.0": - version: 3.0.4 - resolution: "minipass-fetch@npm:3.0.4" + version: 3.0.5 + resolution: "minipass-fetch@npm:3.0.5" dependencies: encoding: "npm:^0.1.13" minipass: "npm:^7.0.3" @@ -6399,7 +6356,7 @@ __metadata: dependenciesMeta: encoding: optional: true - checksum: 3edf72b900e30598567eafe96c30374432a8709e61bb06b87198fa3192d466777e2ec21c52985a0999044fa6567bd6f04651585983a1cbb27e2c1770a07ed2a2 + checksum: c669948bec1373313aaa8f104b962a3ced9f45c49b26366a4b0ae27ccdfa9c5740d72c8a84d3f8623d7a61c5fc7afdfda44789008c078f61a62441142efc4a97 languageName: node linkType: hard @@ -6446,10 +6403,10 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4": - version: 7.0.4 - resolution: "minipass@npm:7.0.4" - checksum: e864bd02ceb5e0707696d58f7ce3a0b89233f0d686ef0d447a66db705c0846a8dc6f34865cd85256c1472ff623665f616b90b8ff58058b2ad996c5de747d2d18 +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.1.2": + version: 7.1.2 + resolution: "minipass@npm:7.1.2" + checksum: c25f0ee8196d8e6036661104bacd743785b2599a21de5c516b32b3fa2b83113ac89a2358465bc04956baab37ffb956ae43be679b2262bf7be15fce467ccd7950 languageName: node linkType: hard @@ -6799,19 +6756,19 @@ __metadata: linkType: hard "node-gyp-build@npm:^4.8.0": - version: 4.8.0 - resolution: "node-gyp-build@npm:4.8.0" + version: 4.8.1 + resolution: "node-gyp-build@npm:4.8.1" bin: node-gyp-build: bin.js node-gyp-build-optional: optional.js node-gyp-build-test: build-test.js - checksum: 80f410ab412df38e84171d3634a5716b6c6f14ecfa4eb971424d289381fb76f8bcbe1b666419ceb2c81060e558fd7c6d70cc0f60832bcca6a1559098925d9657 + checksum: b9297770f96a92e5f2b854f3fd5e4bd418df81d7785a81ab60cec5cf2e5e72dc2c3319808978adc572cfa3885e6b12338cb5f4034bed2cab35f0d76a4b75ccdf languageName: node linkType: hard "node-gyp@npm:latest": - version: 10.1.0 - resolution: "node-gyp@npm:10.1.0" + version: 10.2.0 + resolution: "node-gyp@npm:10.2.0" dependencies: env-paths: "npm:^2.2.0" exponential-backoff: "npm:^3.1.1" @@ -6819,13 +6776,13 @@ __metadata: graceful-fs: "npm:^4.2.6" make-fetch-happen: "npm:^13.0.0" nopt: "npm:^7.0.0" - proc-log: "npm:^3.0.0" + proc-log: "npm:^4.1.0" semver: "npm:^7.3.5" - tar: "npm:^6.1.2" + tar: "npm:^6.2.1" which: "npm:^4.0.0" bin: node-gyp: bin/node-gyp.js - checksum: 89e105e495e66cd4568af3cf79cdeb67d670eb069e33163c7781d3366470a30367c9bd8dea59e46db16370020139e5bf78b1fbc03284cb571754dfaa59744db5 + checksum: 41773093b1275751dec942b985982fd4e7a69b88cae719b868babcef3880ee6168aaec8dcaa8cd0b9fa7c84873e36cc549c6cac6a124ee65ba4ce1f1cc108cfe languageName: node linkType: hard @@ -6839,16 +6796,16 @@ __metadata: linkType: hard "node-releases@npm:^2.0.14": - version: 2.0.14 - resolution: "node-releases@npm:2.0.14" - checksum: 0f7607ec7db5ef1dc616899a5f24ae90c869b6a54c2d4f36ff6d84a282ab9343c7ff3ca3670fe4669171bb1e8a9b3e286e1ef1c131f09a83d70554f855d54f24 + version: 2.0.18 + resolution: "node-releases@npm:2.0.18" + checksum: 241e5fa9556f1c12bafb83c6c3e94f8cf3d8f2f8f904906ecef6e10bcaa1d59aa61212d4651bec70052015fc54bd3fdcdbe7fc0f638a17e6685aa586c076ec4e languageName: node linkType: hard "nodemailer@npm:^6.3.1, nodemailer@npm:^6.4.2": - version: 6.9.13 - resolution: "nodemailer@npm:6.9.13" - checksum: efbc6fc415ec1e1dc1b91530920b0bcfc648183003c3d79718cd54fc2efef4b7dd1917ddd3853ab127e4b5ebd7353903b5859f0ac3ccea487374b076d41ac8b8 + version: 6.9.14 + resolution: "nodemailer@npm:6.9.14" + checksum: 749d1a3ef440d6147c37ad850f5be065d55d87cd46a4470372d4e443593838a5ddd78c69623817a804ddede3212fa28ac069b5b8266e892b27e1dcff75103def languageName: node linkType: hard @@ -6873,24 +6830,13 @@ __metadata: linkType: hard "nopt@npm:^7.0.0": - version: 7.2.0 - resolution: "nopt@npm:7.2.0" + version: 7.2.1 + resolution: "nopt@npm:7.2.1" dependencies: abbrev: "npm:^2.0.0" bin: nopt: bin/nopt.js - checksum: 1e7489f17cbda452c8acaf596a8defb4ae477d2a9953b76eb96f4ec3f62c6b421cd5174eaa742f88279871fde9586d8a1d38fb3f53fa0c405585453be31dff4c - languageName: node - linkType: hard - -"nopt@npm:~1.0.10": - version: 1.0.10 - resolution: "nopt@npm:1.0.10" - dependencies: - abbrev: "npm:1" - bin: - nopt: ./bin/nopt.js - checksum: 4f01ad1e144883a190d70bd6003f26e2f3a899230fe1b0f3310e43779c61cab5ae0063a9209912cd52fc4c552b266b38173853aa9abe27ecb04acbdfdca2e9fc + checksum: 95a1f6dec8a81cd18cdc2fed93e6f0b4e02cf6bdb4501c848752c6e34f9883d9942f036a5e3b21a699047d8a448562d891e67492df68ec9c373e6198133337ae languageName: node linkType: hard @@ -6993,9 +6939,9 @@ __metadata: linkType: hard "object-inspect@npm:^1.13.1": - version: 1.13.1 - resolution: "object-inspect@npm:1.13.1" - checksum: 92f4989ed83422d56431bc39656d4c780348eb15d397ce352ade6b7fec08f973b53744bd41b94af021901e61acaf78fcc19e65bf464ecc0df958586a672700f0 + version: 1.13.2 + resolution: "object-inspect@npm:1.13.2" + checksum: 7ef65583b6397570a17c56f0c1841e0920e83900f2c94638927abb7b81ac08a19c7aae135bd9dcca96208cac0c7332b4650fb927f027b0cf92d71df2990d0561 languageName: node linkType: hard @@ -7183,16 +7129,16 @@ __metadata: linkType: hard "optionator@npm:^0.9.1": - version: 0.9.3 - resolution: "optionator@npm:0.9.3" + version: 0.9.4 + resolution: "optionator@npm:0.9.4" dependencies: - "@aashutoshrathi/word-wrap": "npm:^1.2.3" deep-is: "npm:^0.1.3" fast-levenshtein: "npm:^2.0.6" levn: "npm:^0.4.1" prelude-ls: "npm:^1.2.1" type-check: "npm:^0.4.0" - checksum: fa28d3016395974f7fc087d6bbf0ac7f58ac3489f4f202a377e9c194969f329a7b88c75f8152b33fb08794a30dcd5c079db6bb465c28151357f113d80bbf67da + word-wrap: "npm:^1.2.5" + checksum: a8398559c60aef88d7f353a4f98dcdff6090a4e70f874c827302bf1213d9106a1c4d5fcb68dacb1feb3c30a04c4102f41047aa55d4c576b863d6fc876e001af6 languageName: node linkType: hard @@ -7281,22 +7227,22 @@ __metadata: linkType: hard "pac-proxy-agent@npm:^7.0.1": - version: 7.0.1 - resolution: "pac-proxy-agent@npm:7.0.1" + version: 7.0.2 + resolution: "pac-proxy-agent@npm:7.0.2" dependencies: "@tootallnate/quickjs-emscripten": "npm:^0.23.0" agent-base: "npm:^7.0.2" debug: "npm:^4.3.4" get-uri: "npm:^6.0.1" http-proxy-agent: "npm:^7.0.0" - https-proxy-agent: "npm:^7.0.2" - pac-resolver: "npm:^7.0.0" - socks-proxy-agent: "npm:^8.0.2" - checksum: b9055d13b2a48acf77689c2e510d033486fd90e50a1c7f6bd5f09cd9270bac62ec54bc8fcdd0edbef26e357194cbce70f6794bd99a454d796bc780de6235a61e + https-proxy-agent: "npm:^7.0.5" + pac-resolver: "npm:^7.0.1" + socks-proxy-agent: "npm:^8.0.4" + checksum: bb9b53b32ba98f085fd98ad0ea5e4201498585bf8d9390b3365c057b692b8562997be166d44224878ac216a81f1016c1f55f4e1dec52a6d92e5aa659eba9124c languageName: node linkType: hard -"pac-resolver@npm:^7.0.0": +"pac-resolver@npm:^7.0.1": version: 7.0.1 resolution: "pac-resolver@npm:7.0.1" dependencies: @@ -7318,6 +7264,13 @@ __metadata: languageName: node linkType: hard +"package-json-from-dist@npm:^1.0.0": + version: 1.0.0 + resolution: "package-json-from-dist@npm:1.0.0" + checksum: ac706ec856a5a03f5261e4e48fa974f24feb044d51f84f8332e2af0af04fbdbdd5bbbfb9cbbe354190409bc8307c83a9e38c6672c3c8855f709afb0006a009ea + languageName: node + linkType: hard + "pako@npm:^0.2.5": version: 0.2.9 resolution: "pako@npm:0.2.9" @@ -7441,13 +7394,13 @@ __metadata: languageName: node linkType: hard -"path-scurry@npm:^1.10.2": - version: 1.10.2 - resolution: "path-scurry@npm:1.10.2" +"path-scurry@npm:^1.11.1": + version: 1.11.1 + resolution: "path-scurry@npm:1.11.1" dependencies: lru-cache: "npm:^10.2.0" minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" - checksum: a2bbbe8dc284c49dd9be78ca25f3a8b89300e0acc24a77e6c74824d353ef50efbf163e64a69f4330b301afca42d0e2229be0560d6d616ac4e99d48b4062016b1 + checksum: 5e8845c159261adda6f09814d7725683257fcc85a18f329880ab4d7cc1d12830967eae5d5894e453f341710d5484b8fdbbd4d75181b4d6e1eb2f4dc7aeadc434 languageName: node linkType: hard @@ -7497,10 +7450,10 @@ __metadata: languageName: node linkType: hard -"picocolors@npm:^1.0.0": - version: 1.0.0 - resolution: "picocolors@npm:1.0.0" - checksum: a2e8092dd86c8396bdba9f2b5481032848525b3dc295ce9b57896f931e63fc16f79805144321f72976383fc249584672a75cc18d6777c6b757603f372f745981 +"picocolors@npm:^1.0.0, picocolors@npm:^1.0.1": + version: 1.0.1 + resolution: "picocolors@npm:1.0.1" + checksum: fa68166d1f56009fc02a34cdfd112b0dd3cf1ef57667ac57281f714065558c01828cdf4f18600ad6851cbe0093952ed0660b1e0156bddf2184b6aaf5817553a5 languageName: node linkType: hard @@ -7615,11 +7568,11 @@ __metadata: linkType: hard "pm2@npm:^5.1.0": - version: 5.3.1 - resolution: "pm2@npm:5.3.1" + version: 5.4.2 + resolution: "pm2@npm:5.4.2" dependencies: "@pm2/agent": "npm:~2.0.0" - "@pm2/io": "npm:~5.0.0" + "@pm2/io": "npm:~6.0.1" "@pm2/js-api": "npm:~0.8.0" "@pm2/pm2-version-check": "npm:latest" async: "npm:~3.2.0" @@ -7634,6 +7587,7 @@ __metadata: enquirer: "npm:2.3.6" eventemitter2: "npm:5.0.1" fclone: "npm:1.0.11" + js-yaml: "npm:~4.1.0" mkdirp: "npm:1.0.4" needle: "npm:2.4.0" pidusage: "npm:~3.0" @@ -7647,7 +7601,6 @@ __metadata: source-map-support: "npm:0.5.21" sprintf-js: "npm:1.1.2" vizion: "npm:~2.2.1" - yamljs: "npm:0.3.0" dependenciesMeta: pm2-sysmonit: optional: true @@ -7656,7 +7609,7 @@ __metadata: pm2-dev: bin/pm2-dev pm2-docker: bin/pm2-docker pm2-runtime: bin/pm2-runtime - checksum: cbc04d6fd4af325f694f6fc1e6e41384ef91dd72a57a1e7b4c4bd38c8d735d4fe5578bfc3f4fba08b129c57f1ffa7cd2fdec2c63db2de8c38e1f2852a3bccd5e + checksum: 365967a49193bbbe5bb397597d5384e5459f320d526f1a1544cfe5c695f6040bfa3487a3159cdf19748079e74eee77432959822f54af1c969927c9b052ee0009 languageName: node linkType: hard @@ -7699,10 +7652,10 @@ __metadata: languageName: node linkType: hard -"proc-log@npm:^3.0.0": - version: 3.0.0 - resolution: "proc-log@npm:3.0.0" - checksum: 02b64e1b3919e63df06f836b98d3af002b5cd92655cab18b5746e37374bfb73e03b84fe305454614b34c25b485cc687a9eebdccf0242cda8fda2475dd2c97e02 +"proc-log@npm:^4.1.0, proc-log@npm:^4.2.0": + version: 4.2.0 + resolution: "proc-log@npm:4.2.0" + checksum: 4e1394491b717f6c1ade15c570ecd4c2b681698474d3ae2d303c1e4b6ab9455bd5a81566211e82890d5a5ae9859718cc6954d5150bb18b09b72ecb297beae90a languageName: node linkType: hard @@ -7849,19 +7802,19 @@ __metadata: languageName: node linkType: hard -"pug-code-gen@npm:^3.0.2": - version: 3.0.2 - resolution: "pug-code-gen@npm:3.0.2" +"pug-code-gen@npm:^3.0.3": + version: 3.0.3 + resolution: "pug-code-gen@npm:3.0.3" dependencies: constantinople: "npm:^4.0.1" doctypes: "npm:^1.1.0" js-stringify: "npm:^1.0.2" pug-attrs: "npm:^3.0.0" - pug-error: "npm:^2.0.0" - pug-runtime: "npm:^3.0.0" + pug-error: "npm:^2.1.0" + pug-runtime: "npm:^3.0.1" void-elements: "npm:^3.1.0" with: "npm:^7.0.0" - checksum: 8245ba433a870fc66fec2cc2548cdd45ccd59b71844fa72cc63a7c310a4e35e071bd8a048a97e7d23282ae2a9d16a7d7cef0cfb2e9a37f5f460895b283160089 + checksum: 1918b2a75794b730ee29fc2278658ff2ccb74445742c175c55b18e414cf038e5ac5802e71db070b08f92c5304a66e141dc2261e401be4d5884f1c0bcfb3194ee languageName: node linkType: hard @@ -7872,10 +7825,10 @@ __metadata: languageName: node linkType: hard -"pug-error@npm:^2.0.0": - version: 2.0.0 - resolution: "pug-error@npm:2.0.0" - checksum: c5372d018c897c1d6a141dd803c50957feecfda1f3d84a6adc6149801315d6c7f8c28b05f3e186d98d774fc9718699d1e1caa675630dd3c4453f8c5ec4e4a986 +"pug-error@npm:^2.0.0, pug-error@npm:^2.1.0": + version: 2.1.0 + resolution: "pug-error@npm:2.1.0" + checksum: 9aefacfa156f0eb439ddab86c7136f998a532481a80665c9fb6b998afeea5bc8c4f83eb6ad8a4c7804c44927737df913b768b713995e6892112bbc05762e5415 languageName: node linkType: hard @@ -8052,10 +8005,10 @@ __metadata: linkType: hard "pug@npm:^3.0.1": - version: 3.0.2 - resolution: "pug@npm:3.0.2" + version: 3.0.3 + resolution: "pug@npm:3.0.3" dependencies: - pug-code-gen: "npm:^3.0.2" + pug-code-gen: "npm:^3.0.3" pug-filters: "npm:^4.0.0" pug-lexer: "npm:^5.0.1" pug-linker: "npm:^4.0.0" @@ -8063,7 +8016,7 @@ __metadata: pug-parser: "npm:^6.0.0" pug-runtime: "npm:^3.0.1" pug-strip-comments: "npm:^2.0.0" - checksum: 4bb4cab1abbdfc3cc3e7d9a7813e204ca3febf8819f81f8c4279941fb1567939015a720a1f6de8a51cf1fa6ad134d1e783c9b66f7ef8c29d3feaf8a2445f7237 + checksum: a88364757512e3b9af024c008f23b910de049659655b5d9e6ca42f996d7849ce1aab059f61e2d44ccce0dde5ff291995682338c285e9f76f3e5bfa02de9c481b languageName: node linkType: hard @@ -8094,11 +8047,11 @@ __metadata: linkType: hard "qs@npm:^6.11.0, qs@npm:^6.9.4": - version: 6.12.1 - resolution: "qs@npm:6.12.1" + version: 6.12.3 + resolution: "qs@npm:6.12.3" dependencies: side-channel: "npm:^1.0.6" - checksum: 035bcad2a1ab0175bac7a74c904c15913bdac252834149ccff988c93a51de02642fe7be10e43058ba4dc4094bb28ce9b59d12b9e91d40997f445cfde3ecc1c29 + checksum: 486d80cfa5e12886de6fe15a5aa2b3c7023bf4461f949a742022c3ae608499dbaebcb57b1f15c1f59d86356772969028768b33c1a7c01e76d99f149239e63d59 languageName: node linkType: hard @@ -8201,7 +8154,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0": +"readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.2": version: 3.6.2 resolution: "readable-stream@npm:3.6.2" dependencies: @@ -8514,9 +8467,9 @@ __metadata: linkType: hard "sax@npm:^1.2.4": - version: 1.3.0 - resolution: "sax@npm:1.3.0" - checksum: bb571b31d30ecb0353c2ff5f87b117a03e5fb9eb4c1519141854c1a8fbee0a77ddbe8045f413259e711833aa03da210887df8527d19cdc55f299822dbf4b34de + version: 1.4.1 + resolution: "sax@npm:1.4.1" + checksum: b1c784b545019187b53a0c28edb4f6314951c971e2963a69739c6ce222bfbc767e54d320e689352daba79b7d5e06d22b5d7113b99336219d6e93718e2f99d335 languageName: node linkType: hard @@ -8534,7 +8487,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.3.0, semver@npm:^5.5.0, semver@npm:^5.6.0, semver@npm:^5.7.0, semver@npm:^5.7.1": +"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.5.0, semver@npm:^5.6.0, semver@npm:^5.7.0, semver@npm:^5.7.1": version: 5.7.2 resolution: "semver@npm:5.7.2" bin: @@ -8553,13 +8506,11 @@ __metadata: linkType: hard "semver@npm:^7.2, semver@npm:^7.2.1, semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.3": - version: 7.6.0 - resolution: "semver@npm:7.6.0" - dependencies: - lru-cache: "npm:^6.0.0" + version: 7.6.3 + resolution: "semver@npm:7.6.3" bin: semver: bin/semver.js - checksum: 1b41018df2d8aca5a1db4729985e8e20428c650daea60fcd16e926e9383217d00f574fab92d79612771884a98d2ee2a1973f49d630829a8d54d6570defe62535 + checksum: 36b1fbe1a2b6f873559cd57b238f1094a053dbfd997ceeb8757d79d1d2089c56d1321b9f1069ce263dc64cfa922fa1d2ad566b39426fe1ac6c723c1487589e10 languageName: node linkType: hard @@ -8711,7 +8662,7 @@ __metadata: languageName: node linkType: hard -"shimmer@npm:^1.1.0, shimmer@npm:^1.2.0": +"shimmer@npm:^1.2.0": version: 1.2.1 resolution: "shimmer@npm:1.2.1" checksum: aa0d6252ad1c682a4fdfda69e541be987f7a265ac7b00b1208e5e48cc68dc55f293955346ea4c71a169b7324b82c70f8400b3d3d2d60b2a7519f0a3522423250 @@ -8842,18 +8793,18 @@ __metadata: languageName: node linkType: hard -"socks-proxy-agent@npm:^8.0.2, socks-proxy-agent@npm:^8.0.3": - version: 8.0.3 - resolution: "socks-proxy-agent@npm:8.0.3" +"socks-proxy-agent@npm:^8.0.2, socks-proxy-agent@npm:^8.0.3, socks-proxy-agent@npm:^8.0.4": + version: 8.0.4 + resolution: "socks-proxy-agent@npm:8.0.4" dependencies: agent-base: "npm:^7.1.1" debug: "npm:^4.3.4" - socks: "npm:^2.7.1" - checksum: c2112c66d6322e497d68e913c3780f3683237fd394bfd480b9283486a86e36095d0020db96145d88f8ccd9cc73261b98165b461f9c1bf5dc17abfe75c18029ce + socks: "npm:^2.8.3" + checksum: c8e7c2b398338b49a0a0f4d2bae5c0602aeeca6b478b99415927b6c5db349ca258448f2c87c6958ebf83eea17d42cbc5d1af0bfecb276cac10b9658b0f07f7d7 languageName: node linkType: hard -"socks@npm:^2.7.1": +"socks@npm:^2.8.3": version: 2.8.3 resolution: "socks@npm:2.8.3" dependencies: @@ -8948,9 +8899,9 @@ __metadata: linkType: hard "spdx-license-ids@npm:^3.0.0": - version: 3.0.17 - resolution: "spdx-license-ids@npm:3.0.17" - checksum: 8f6c6ae02ebb25b4ca658b8990d9e8a8f8d8a95e1d8b9fd84d87eed80a7dc8f8073d6a8d50b8a0295c0e8399e1f8814f5c00e2985e6bf3731540a16f7241cbf1 + version: 3.0.18 + resolution: "spdx-license-ids@npm:3.0.18" + checksum: 45fdbb50c4bbe364720ef0acd19f4fc1914d73ba1e2b1ce9db21ee12d7f9e8bf14336289f6ad3d5acac3dc5b91aafe61e9c652d5806b31cbb8518a14979a16ff languageName: node linkType: hard @@ -8997,11 +8948,11 @@ __metadata: linkType: hard "ssri@npm:^10.0.0": - version: 10.0.5 - resolution: "ssri@npm:10.0.5" + version: 10.0.6 + resolution: "ssri@npm:10.0.6" dependencies: minipass: "npm:^7.0.3" - checksum: 453f9a1c241c13f5dfceca2ab7b4687bcff354c3ccbc932f35452687b9ef0ccf8983fd13b8a3baa5844c1a4882d6e3ddff48b0e7fd21d743809ef33b80616d79 + checksum: f92c1b3cc9bfd0a925417412d07d999935917bc87049f43ebec41074661d64cf720315661844106a77da9f8204b6d55ae29f9514e673083cae39464343af2a8b languageName: node linkType: hard @@ -9270,8 +9221,8 @@ __metadata: linkType: hard "systeminformation@npm:^5.7": - version: 5.22.7 - resolution: "systeminformation@npm:5.22.7" + version: 5.22.11 + resolution: "systeminformation@npm:5.22.11" bin: systeminformation: lib/cli.js conditions: (os=darwin | os=linux | os=win32 | os=freebsd | os=openbsd | os=netbsd | os=sunos | os=android) @@ -9291,7 +9242,7 @@ __metadata: languageName: node linkType: hard -"tar@npm:^6.1.11, tar@npm:^6.1.2": +"tar@npm:^6.1.11, tar@npm:^6.2.1": version: 6.2.1 resolution: "tar@npm:6.2.1" dependencies: @@ -9418,13 +9369,11 @@ __metadata: linkType: hard "touch@npm:^3.1.0": - version: 3.1.0 - resolution: "touch@npm:3.1.0" - dependencies: - nopt: "npm:~1.0.10" + version: 3.1.1 + resolution: "touch@npm:3.1.1" bin: - nodetouch: ./bin/nodetouch.js - checksum: ece1d9693fbc9b73d8a6d902537b787b5685ac1aeab7562857c50e6671415a73c985055393442b518f4ac37b85c3e7a3e6c36af71142fed13b8bb04fb6664936 + nodetouch: bin/nodetouch.js + checksum: 853e763a1f4903302c5654ed353f84ad85baf757dac62c2d37ab67e0477cfd271e8c64771fcfad42310aff7c9d284ddb435ee5ca13ff36d0f3693fedd8e971d1 languageName: node linkType: hard @@ -9472,9 +9421,9 @@ __metadata: linkType: hard "tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.6.2": - version: 2.6.2 - resolution: "tslib@npm:2.6.2" - checksum: bd26c22d36736513980091a1e356378e8b662ded04204453d353a7f34a4c21ed0afc59b5f90719d4ba756e581a162ecbf93118dc9c6be5acf70aa309188166ca + version: 2.6.3 + resolution: "tslib@npm:2.6.3" + checksum: 52109bb681f8133a2e58142f11a50e05476de4f075ca906d13b596ae5f7f12d30c482feb0bff167ae01cfc84c5803e575a307d47938999246f5a49d174fc558c languageName: node linkType: hard @@ -9649,11 +9598,11 @@ __metadata: linkType: hard "uglify-js@npm:^3.1.4": - version: 3.17.4 - resolution: "uglify-js@npm:3.17.4" + version: 3.19.0 + resolution: "uglify-js@npm:3.19.0" bin: uglifyjs: bin/uglifyjs - checksum: 4c0b800e0ff192079d2c3ce8414fd3b656a570028c7c79af5c29c53d5c532b68bbcae4ad47307f89c2ee124d11826fff7a136b59d5c5bb18422bcdf5568afe1e + checksum: 44b37f88805565ba478665f4d5560388a072b314c38708046a5b97ca49ec40cb0d34414daff77d44695991098b7596536847e7d87b4590f457fc757e1d2904cc languageName: node linkType: hard @@ -9738,17 +9687,17 @@ __metadata: languageName: node linkType: hard -"update-browserslist-db@npm:^1.0.13": - version: 1.0.13 - resolution: "update-browserslist-db@npm:1.0.13" +"update-browserslist-db@npm:^1.1.0": + version: 1.1.0 + resolution: "update-browserslist-db@npm:1.1.0" dependencies: - escalade: "npm:^3.1.1" - picocolors: "npm:^1.0.0" + escalade: "npm:^3.1.2" + picocolors: "npm:^1.0.1" peerDependencies: browserslist: ">= 4.21.0" bin: update-browserslist-db: cli.js - checksum: 9074b4ef34d2ed931f27d390aafdd391ee7c45ad83c508e8fed6aaae1eb68f81999a768ed8525c6f88d4001a4fbf1b8c0268f099d0e8e72088ec5945ac796acf + checksum: d70b9efeaf4601aadb1a4f6456a7a5d9118e0063d995866b8e0c5e0cf559482671dab6ce7b079f9536b06758a344fbd83f974b965211e1c6e8d1958540b0c24c languageName: node linkType: hard @@ -9782,7 +9731,7 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^3.1.0, uuid@npm:^3.2.1, uuid@npm:^3.3.2, uuid@npm:^3.3.3": +"uuid@npm:^3.1.0, uuid@npm:^3.3.2, uuid@npm:^3.3.3": version: 3.4.0 resolution: "uuid@npm:3.4.0" bin: @@ -9975,32 +9924,32 @@ __metadata: linkType: hard "winston-transport@npm:^4.7.0": - version: 4.7.0 - resolution: "winston-transport@npm:4.7.0" + version: 4.7.1 + resolution: "winston-transport@npm:4.7.1" dependencies: - logform: "npm:^2.3.2" - readable-stream: "npm:^3.6.0" + logform: "npm:^2.6.1" + readable-stream: "npm:^3.6.2" triple-beam: "npm:^1.3.0" - checksum: c8eae7b110e68396edcf26aec86608bd8ac98f3cc05961064e2e577b023d9c4aa485546cacba84efaf48b7d6b1e282dc211fd959ee16cbd31d34476d96daea43 + checksum: bc48c921ec9b4a71c1445bf274aa6b00c01089a6c26fc0b19534f8a32fa2710c6766c9e6db53a23492c20772934025d312dd9fb08df157ccb6579ad6b9dae9a7 languageName: node linkType: hard "winston@npm:^3.1.0, winston@npm:^3.3.3": - version: 3.13.0 - resolution: "winston@npm:3.13.0" + version: 3.13.1 + resolution: "winston@npm:3.13.1" dependencies: "@colors/colors": "npm:^1.6.0" "@dabh/diagnostics": "npm:^2.0.2" async: "npm:^3.2.3" is-stream: "npm:^2.0.0" - logform: "npm:^2.4.0" + logform: "npm:^2.6.0" one-time: "npm:^1.0.0" readable-stream: "npm:^3.4.0" safe-stable-stringify: "npm:^2.3.1" stack-trace: "npm:0.0.x" triple-beam: "npm:^1.3.0" winston-transport: "npm:^4.7.0" - checksum: 436675598359af27e4eabde2ce578cf77da893ffd57d0479f037fef939e8eb721031f0102b14399eee93b3412b545946c431d1fff23db3beeac2ffa395537f7b + checksum: bc78202708800f74b94a2cc4fbdd46569dea90f939ad2149a936b2deee612d63a512f9e5725251349090bc12ba35351dd67336b3c92bf094892f9ea03d34fdc4 languageName: node linkType: hard @@ -10026,6 +9975,13 @@ __metadata: languageName: node linkType: hard +"word-wrap@npm:^1.2.5": + version: 1.2.5 + resolution: "word-wrap@npm:1.2.5" + checksum: 1ec6f6089f205f83037be10d0c4b34c9183b0b63fca0834a5b3cee55dd321429d73d40bb44c8fc8471b5203d6e8f8275717f49a8ff4b2b0ab41d7e1b563e0854 + languageName: node + linkType: hard + "wordwrap@npm:0.0.2": version: 0.0.2 resolution: "wordwrap@npm:0.0.2" @@ -10103,9 +10059,9 @@ __metadata: languageName: node linkType: hard -"ws@npm:^7.0.0": - version: 7.5.9 - resolution: "ws@npm:7.5.9" +"ws@npm:^7.0.0, ws@npm:~7.5.10": + version: 7.5.10 + resolution: "ws@npm:7.5.10" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ^5.0.2 @@ -10114,13 +10070,13 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 171e35012934bd8788150a7f46f963e50bac43a4dc524ee714c20f258693ac4d3ba2abadb00838fdac42a47af9e958c7ae7e6f4bc56db047ba897b8a2268cf7c + checksum: 9c796b84ba80ffc2c2adcdfc9c8e9a219ba99caa435c9a8d45f9ac593bba325563b3f83edc5eb067cc6d21b9a6bf2c930adf76dd40af5f58a5ca6859e81858f0 languageName: node linkType: hard "ws@npm:^8.15.1, ws@npm:^8.8.1": - version: 8.16.0 - resolution: "ws@npm:8.16.0" + version: 8.18.0 + resolution: "ws@npm:8.18.0" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -10129,22 +10085,7 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 7c511c59e979bd37b63c3aea4a8e4d4163204f00bd5633c053b05ed67835481995f61a523b0ad2b603566f9a89b34cb4965cb9fab9649fbfebd8f740cea57f17 - languageName: node - linkType: hard - -"ws@npm:~7.4.0": - version: 7.4.6 - resolution: "ws@npm:7.4.6" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: 150e3f917b7cde568d833a5ea6ccc4132e59c38d04218afcf2b6c7b845752bd011a9e0dc1303c8694d3c402a0bdec5893661a390b71ff88f0fc81a4e4e66b09c + checksum: 70dfe53f23ff4368d46e4c0b1d4ca734db2c4149c6f68bc62cb16fc21f753c47b35fcc6e582f3bdfba0eaeb1c488cddab3c2255755a5c3eecb251431e42b3ff6 languageName: node linkType: hard @@ -10176,19 +10117,6 @@ __metadata: languageName: node linkType: hard -"yamljs@npm:0.3.0": - version: 0.3.0 - resolution: "yamljs@npm:0.3.0" - dependencies: - argparse: "npm:^1.0.7" - glob: "npm:^7.0.5" - bin: - json2yaml: ./bin/json2yaml - yaml2json: ./bin/yaml2json - checksum: 041ccb467b04e0ebfa8224fceca03a28fb28666f46d8ac82ba19b2b118d44604566c17def5cb5ae6681fcedd903affbb42f757706b1e5440dcd304d5f802ef3c - languageName: node - linkType: hard - "yargs-parser@npm:13.1.2, yargs-parser@npm:^13.1.2": version: 13.1.2 resolution: "yargs-parser@npm:13.1.2" diff --git a/src/GlobalStateProvider.tsx b/src/GlobalStateProvider.tsx index 7a0def8a..d7c988a6 100644 --- a/src/GlobalStateProvider.tsx +++ b/src/GlobalStateProvider.tsx @@ -1,17 +1,10 @@ -import { getWalletBySource, WalletAccount } from '@talismn/connect-wallets'; -import { getSdkError } from '@walletconnect/utils'; +import { WalletAccount } from '@talismn/connect-wallets'; import { ComponentChildren, createContext } from 'preact'; -import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'preact/compat'; +import { useCallback, useContext, useMemo, useState } from 'preact/compat'; import { useLocation } from 'react-router-dom'; import { config } from './config'; -import { chainIds } from './config/walletConnect'; - -import { useLocalStorage } from './hooks/useLocalStorage'; import { TenantName } from './models/Tenant'; import { ThemeName } from './models/Theme'; -import { initiateMetamaskInjectedAccount, WALLET_SOURCE_METAMASK } from './services/metamask'; -import { storageService } from './services/storage/local'; -import { walletConnectService } from './services/walletConnect'; export interface GlobalState { dAppName: string; diff --git a/src/app.tsx b/src/app.tsx index 880f24e4..d6d98b59 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,10 +1,18 @@ import { Route, Routes } from 'react-router-dom'; -import Landing from './pages/landing'; +import { SwapPage } from './pages/swap'; +import '../App.css'; +import { ProgressPage } from './pages/progress'; export function App() { return ( - - } /> - + <> + + } /> + } /> + +
+ {/* This is where the dialogs/modals are rendered. It is placed here because it is the highest point in the app where the tailwind data-theme is available */} +
+ ); } diff --git a/src/assets/CloseIcon.tsx b/src/assets/CloseIcon.tsx new file mode 100644 index 00000000..e1e640f8 --- /dev/null +++ b/src/assets/CloseIcon.tsx @@ -0,0 +1,11 @@ +import { FC } from 'preact/compat'; + +interface Props { + className?: string; +} + +export const CloseIcon: FC = ({ className = 'dark:fill-white fill-black' }) => ( + + + +); diff --git a/src/assets/coins/USDC_POLYGON.svg b/src/assets/coins/USDC_POLYGON.svg new file mode 100644 index 00000000..73055b88 --- /dev/null +++ b/src/assets/coins/USDC_POLYGON.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/assets/logo/white.png b/src/assets/logo/white.png new file mode 100644 index 0000000000000000000000000000000000000000..28630abc8ad89d9cc531b1f10db055f143431c5a GIT binary patch literal 2283 zcmV)*XbYAl?e1tsr&!Y1 zE>v=WXt`Xt(7#X0GjWJKxX`#xs$nM2j4Q*r3s+hm_fHMqWa9Wa)!jB7-Cg+V>!5X9 zZbtk)mZ5gxcB>p;w`atcazf|`;r`GDu6+2-1^quADZcwZKWjKUJ3FY+WBaAJvaRJv z@a3+hYv)%|sJ&hM&rbG@`|GWA?{cQW{h!MF;KFK*cJh7by>xY|dyj9n^E~sFpBpmQ zF42vYDV*k2Nuj)aWoItDm7gmcLSMp17anq6f{sq^k9S72>wf3ym0K15+&|L#|Gpz1 zjr)3s?8~jRyI8xh3OxVf^7~sN@O9R5&y2jcS-J4h<#i_!xV*G%?*|^=d6_H8#Mept zW<3x^6=$(4VFMeskwOKmha>z z%F}@3V3Y?>cf7{LS~G6Sb4AEvm2pnUS}?LUozrTldr1U#=UHZ^FHH-=3;?G*%)s;` z-+_GgsyyIl%04N|Mw#nmJE*J+Lk6=WP?V=7(`(&yaJcBD*TGoZrXl6DLI8f9az2or85F3p^aWqQhME?8 za+j%v1*do$8W^Ufmlx9!EqRRc451xQ?QT`#7m^F?wE^7$V9y+jK-wj2;_cJ8@+qMm zsTk#1NMTT3c`SKKB?*Arl!~%!V81`jkC~;)_yEGTQK{(_oC#0`(zitVnPdQNb{niO zc;(4b@<91pBVW2xkZqCG$zabuQJYfW)-e_>5;nqyjwIYk*zJ1bmt|pnvA-MgD?_(? zQ%RV0tglob?-#W)qHP>bb132)I5JEfKS~}eip^$Xqj)d@jTR_DzW(@Wl%Y_5l1`LO47(y@;&jIi1P>X4yrLDytegcA?bNw z$8w$O7#up1po$Sv9-ZRrA&*vc=2R6(Z@H9LVzZJmFfS^{F?J=#0aM4K8y*8hx3zkP zd!>FKk?baCP7Qx9)Tz49nuzmC(kV4Vj!m}bd?8ZHu@HX7 zTJ1by5P@|J+p^Q&?OJHz@qpA2$705mf;K^(nL|;YB>aGXO-5(q4f4V$%u2$u&(Ow! zwNY>ki|0Py<@6~vlRqcyu@cG|H3V}ETg|IF@O;#Y%&HQAVTf;zI6u+wWI&RR|%rp;?P zF#O<@vQ4erW6V3^V7pa(_3>9P1~v=H0NjYxW~Q%XBv5wdIVb9pbSz~oP{zX` z$%3I~9>0qb+FI5M)M9%b#D1OrhlukY8J9l@x4>M-_PiaJW zV2=XwjzZ5Q0Ysd;0e_TN%}e%FsdYU3q?E_YWZNdS)9-5kC|O82N)65fyOJ>gf4-(q zvPUua*=NIYr~wdv*PGdxI8Eh=dC&C0K0LPn5bQ-D6%vkOr=}|g(iUe|k^$0`wnaD6 z+`~30lgo`0R30yvo$)uc=mswReo}7*Z67_s*$^_A%R91;QiHo{Ao-%3Ac${Npg`wI z%r}PxS+0{*9xwY2`{$EUrtG8C;OFb&lqj4Xa}7UdAEifeI9#Pbp_&@X_VeT0Ji|Vlo3JPrFug& zc*bjlbHSc}+zMrWDFI%FS`o}$7^BS)$hZUhOcL>htwM21{B8<^FN-Q_|F>>-pH z` ( + + {({ account, chain, openChainModal, openConnectModal, authenticationStatus, mounted }) => { + const ready = mounted && authenticationStatus !== 'loading'; + const connected = + ready && account && chain && (!authenticationStatus || authenticationStatus === 'authenticated'); + + return ( + + ); + }} + +); + +interface AssetNumericInputProps { + tokenType?: InputTokenType | OutputTokenType; + tokenSymbol?: string; + onClick: () => void; + additionalText?: string; + disabled?: boolean; + readOnly?: boolean; + registerInput: UseFormRegisterReturn; +} + +export const AssetNumericInput: FC = ({ + additionalText, + tokenType, + tokenSymbol, + onClick, + registerInput, + ...rest +}) => { + const memoizedAssetButton = useMemo( + () => , + [tokenType, onClick], + ); + + return ( +
+
+ {memoizedAssetButton} +
+ {additionalText ?

{additionalText}

: } +
+ + +
+ ); +}; diff --git a/src/components/BenefitsList/index.tsx b/src/components/BenefitsList/index.tsx new file mode 100644 index 00000000..c7dc5888 --- /dev/null +++ b/src/components/BenefitsList/index.tsx @@ -0,0 +1,37 @@ +import { CheckIcon } from '@heroicons/react/20/solid'; +import { FC } from 'preact/compat'; +import Big from 'big.js'; + +/// The factor we use to derive the amount we estimate the user to save using our transfer method. +const AMOUNT_SAVED_FACTOR = 0.03; + +interface BenefitsListProps { + amount: Big | undefined; + currency: string; +} + +export const BenefitsList: FC = ({ amount, currency }) => ( +
    +
  • + +

    + You could save{' '} + + up to {amount ? amount.mul(AMOUNT_SAVED_FACTOR).toFixed(2) : '0.0'} {currency.toUpperCase()} + +

    +
  • +
  • + +

    + Should arrive in 5 minutes +

    +
  • +
  • + +

    + Verify super fast with your Tax ID +

    +
  • +
+); diff --git a/src/components/Box/index.tsx b/src/components/Box/index.tsx new file mode 100644 index 00000000..4874f0f0 --- /dev/null +++ b/src/components/Box/index.tsx @@ -0,0 +1,12 @@ +import { FC } from 'preact/compat'; + +export const Box: FC<{ className: string }> = ({ children, className }) => ( +
+ {children} +
+); diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx new file mode 100644 index 00000000..99c6c9df --- /dev/null +++ b/src/components/Dialog/index.tsx @@ -0,0 +1,96 @@ +import { Modal } from 'react-daisyui'; +import { FC, createPortal, useCallback, useEffect, useRef, useState } from 'preact/compat'; + +import { CloseButton } from '../buttons/CloseButton'; + +interface DialogProps { + visible: boolean; + onClose: () => void; + headerText?: string; + content: JSX.Element; + actions?: JSX.Element; + form?: { + onSubmit: (event?: Event) => void | Promise; + className?: string; + }; + id?: string; +} + +export const Dialog: FC = ({ visible, onClose, headerText, content, actions, id, form }) => { + const ref = useRef(null); + + // If it was the form submission we want to only close the dialog without calling onClose + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleClose = useCallback( + (dialog: HTMLDialogElement) => { + if (isSubmitting) { + setIsSubmitting(false); + dialog.close(); + return; + } + + dialog.close(); + onClose(); + }, + [isSubmitting, onClose], + ); + + const closeListener = useCallback(() => { + const dialog = ref.current; + if (dialog) { + handleClose(dialog); + } + }, [handleClose]); + + // Manage native events and visibility + useEffect(() => { + const dialog = ref.current; + if (dialog) { + dialog.addEventListener('close', closeListener); + if (visible && !dialog.open) { + dialog.showModal(); + } else if (!visible && dialog.open) { + dialog.close(); + } + + return () => { + dialog.removeEventListener('close', closeListener); + }; + } + }, [visible, closeListener, headerText]); + + const handleFormSubmit = (event: Event) => { + if (form) { + setIsSubmitting(true); + event.preventDefault(); + form.onSubmit(event); + } + }; + + const container = document.getElementById('modals'); + if (!container) return null; + + const modalBody = ( + <> + {content} + {actions} + + ); + + return createPortal( + + + {headerText} + + {form ? ( +
+ {modalBody} +
+ ) : ( + modalBody + )} +
, + container, + ); +}; diff --git a/src/components/ExchangeRate/index.tsx b/src/components/ExchangeRate/index.tsx new file mode 100644 index 00000000..a38c895e --- /dev/null +++ b/src/components/ExchangeRate/index.tsx @@ -0,0 +1,22 @@ +import { InputTokenDetails, OutputTokenDetails } from '../../constants/tokenConfig'; +import { UseTokenOutAmountResult } from '../../hooks/nabla/useTokenAmountOut'; +import { FC } from 'preact/compat'; + +interface ExchangeRateProps { + fromToken?: InputTokenDetails; + toToken?: OutputTokenDetails; + tokenOutData: UseTokenOutAmountResult; +} + +export const ExchangeRate: FC = ({ tokenOutData, fromToken, toToken }) => { + const exchangeRate = + fromToken !== undefined && toToken !== undefined && !tokenOutData.isLoading && tokenOutData.data ? ( + <>{`1 ${fromToken.assetSymbol} = ${Number(tokenOutData.data.effectiveExchangeRate).toFixed(2)} ${ + toToken.stellarAsset.code.string + }`} + ) : ( + `-` + ); + + return

{exchangeRate}

; +}; diff --git a/src/components/FeeCollapse/index.tsx b/src/components/FeeCollapse/index.tsx new file mode 100644 index 00000000..63158c1e --- /dev/null +++ b/src/components/FeeCollapse/index.tsx @@ -0,0 +1,71 @@ +import { FC } from 'preact/compat'; +import { useState } from 'preact/hooks'; +import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/20/solid'; +import LocalGasStationIcon from '@mui/icons-material/LocalGasStation'; +import Big from 'big.js'; +import { roundDownToSignificantDecimals } from '../../helpers/parseNumbers'; +import { OUTPUT_TOKEN_CONFIG, OutputTokenType } from '../../constants/tokenConfig'; + +const FEES_RATE = 0.05; // 0.5% fee rate + +function calculateTotalReceive(toAmount: string): string { + const totalReceive = Number(toAmount) * (1 - FEES_RATE); + return roundDownToSignificantDecimals(new Big(totalReceive || 0), 2).toString(); +} + +function calculateFeesUSD(fromAmount: string): string { + const totalReceiveUSD = Number(fromAmount) * (1 - FEES_RATE); + const feesCost = Number(fromAmount) - Number(totalReceiveUSD); + return roundDownToSignificantDecimals(new Big(feesCost || 0), 2).toString(); +} + +interface CollapseProps { + fromAmount?: string; + toAmount?: string; + toCurrency?: OutputTokenType; +} + +export const FeeCollapse: FC = ({ fromAmount, toAmount, toCurrency }) => { + const [isOpen, setIsOpen] = useState(false); + + const toggleIsOpen = () => setIsOpen((state) => !state); + + const outputToken = toCurrency ? OUTPUT_TOKEN_CONFIG[toCurrency] : undefined; + + const chevron = isOpen ? ( + + ) : ( + + ); + + const totalReceive = calculateTotalReceive(toAmount || '0'); + const feesCost = calculateFeesUSD(fromAmount || '0'); + + return ( +
+ +
+

+ + {totalReceive} {outputToken?.stellarAsset.code.string} + +  is what you will receive, after fees +

+
+

Show fees

+ {chevron} +
+
+
+
+
+

Total fees

+
+ + ${feesCost} +
+
+
+
+ ); +}; diff --git a/src/components/GenericEvent.tsx b/src/components/GenericEvent.tsx index f407feec..1715522d 100644 --- a/src/components/GenericEvent.tsx +++ b/src/components/GenericEvent.tsx @@ -6,6 +6,8 @@ export enum EventStatus { Waiting = 'waiting', } +export type RenderEventHandler = (event: string, status: EventStatus) => void; + export interface GenericEvent { value: string; status: EventStatus; diff --git a/src/components/InputKeys/AmountSelector.tsx b/src/components/InputKeys/AmountSelector.tsx deleted file mode 100644 index 1c172da4..00000000 --- a/src/components/InputKeys/AmountSelector.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { Button, Range } from 'react-daisyui'; -import { FieldPath, FieldValues, PathValue, UseFormReturn, useWatch } from 'react-hook-form'; -import { useEffect, useMemo } from 'preact/hooks'; - -import { NumberInput } from '../Nabla/NumberInput'; -import { ChangeEvent } from 'preact/compat'; -import { BalanceInfo } from '../Nabla/BalanceState'; -import BigNumber from 'big.js'; - -interface AmountSelectorProps> { - maxBalance: BalanceInfo | undefined; - formFieldName: TFieldName; - form: UseFormReturn; - children?: ReactNode; - onlyShowNumberInput?: boolean; - offrampStarted: boolean; -} - -export function AmountSelector>({ - offrampStarted, - formFieldName, - maxBalance, - form, - children, - onlyShowNumberInput, -}: AmountSelectorProps) { - type K = PathValue; - - const { setError, clearErrors, setValue } = form; - - const amountString: string = useWatch({ - control: form.control, - name: formFieldName, - defaultValue: '0' as K, - }); - - const amountBigDecimal = useMemo(() => { - try { - return new BigNumber(amountString); - } catch { - return undefined; - } - }, [amountString]); - - const amountNumber = useMemo(() => { - try { - return Number(amountString); - } catch { - return undefined; - } - }, [amountString]); - - useEffect(() => { - const determineErrorMessage = (): string | undefined => { - if (amountString.length === 0) return; - if (amountBigDecimal === undefined) return 'Enter a proper number'; - if (maxBalance === undefined) return; - // If we don't want to show the quote result if the balance is lower that the selected at from, - // we can do so uncommenting this line. - if (amountBigDecimal.gt(maxBalance.preciseBigDecimal)) return 'Amount exceeds maximum'; - - if (amountBigDecimal.c[0] !== 0) { - if (amountBigDecimal.e + 1 + maxBalance.decimals < amountBigDecimal.c.length) - return `The number you entered must have at most ${maxBalance.decimals} decimal places`; - } - }; - - const errorMessage = determineErrorMessage(); - if (errorMessage) { - setError(formFieldName, { type: 'custom', message: errorMessage }); - } else { - clearErrors(formFieldName); - } - }, [amountString, amountBigDecimal, formFieldName, maxBalance, setError, clearErrors]); - - if (onlyShowNumberInput === true) { - return ( - - ); - } - - return ( -
-
-
- - - -
-
- ) => { - if (maxBalance === undefined) return; - setValue(formFieldName, (maxBalance.approximateNumber / Number(ev.currentTarget.value)).toString() as K, { - shouldDirty: true, - shouldTouch: false, - }); - }} - /> - {children} -
- ); -} diff --git a/src/components/InputKeys/From.tsx b/src/components/InputKeys/From.tsx deleted file mode 100644 index 590cb6a9..00000000 --- a/src/components/InputKeys/From.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { ChevronDownIcon } from '@heroicons/react/20/solid'; -import { Button } from 'react-daisyui'; -import { FieldPath, FieldValues, UseFormReturn, useFormContext } from 'react-hook-form'; - -import pendulumIcon from '../../assets/pendulum-icon.svg'; -import { TokenDetails } from '../../constants/tokenConfig'; -import { AmountSelector } from './AmountSelector'; -import { BalanceInfo } from '../Nabla/BalanceState'; -import { SwapFormValues } from '../Nabla/schema'; -import { TokenBalance } from '../Nabla/TokenBalance'; - -interface FromProps> { - tokenId: string; - fromToken: TokenDetails | undefined; - onOpenSelector: () => void; - inputHasError: boolean; - fromFormFieldName: TFieldName; - form: UseFormReturn; - tokenBalance: BalanceInfo; - offrampStarted: boolean; -} - -export function From>({ - tokenId, - fromToken, - onOpenSelector, - inputHasError, - fromFormFieldName, - form, - tokenBalance, - offrampStarted, -}: FromProps) { - const { setValue } = useFormContext(); - // we can get rid of this and just load USDC balance, not pass tokenBalance object. - const fromTokenBalance = tokenBalance; - return ( -
-
-
- -
- -
-
- {fromTokenBalance !== undefined && ( -
- - Your Balance: - - - -
- )} -
-
- ); -} diff --git a/src/components/InputKeys/PoolListItem/index.tsx b/src/components/InputKeys/PoolListItem/index.tsx new file mode 100644 index 00000000..02fa83e5 --- /dev/null +++ b/src/components/InputKeys/PoolListItem/index.tsx @@ -0,0 +1,44 @@ +import { Avatar, AvatarProps, Button } from 'react-daisyui'; +import { CheckIcon } from '@heroicons/react/20/solid'; +import { useGetIcon } from '../../../hooks/useGetIcon'; +import { InputTokenType, OutputTokenType } from '../../../constants/tokenConfig'; + +interface PoolListItemProps { + tokenType: T; + tokenSymbol: string; + isSelected?: boolean; + onSelect: (tokenType: T) => void; +} + +export function PoolListItem({ + tokenType, + tokenSymbol, + isSelected, + onSelect, +}: PoolListItemProps) { + const tokenIcon = useGetIcon(tokenType); + + return ( + + ); +} diff --git a/src/components/InputKeys/SelectionModal.tsx b/src/components/InputKeys/SelectionModal.tsx index 3683cad4..3d39d1ff 100644 --- a/src/components/InputKeys/SelectionModal.tsx +++ b/src/components/InputKeys/SelectionModal.tsx @@ -1,134 +1,63 @@ import { Skeleton } from '../Skeleton'; -import { Avatar, AvatarProps, Modal, Button, Input, ButtonProps } from 'react-daisyui'; +import { Input } from 'react-daisyui'; import { ChangeEvent, useMemo, useState } from 'preact/compat'; -import { CheckIcon } from '@heroicons/react/20/solid'; -import { TOKEN_CONFIG, TokenDetails, TokenType } from '../../constants/tokenConfig'; +import { InputTokenType, OutputTokenType } from '../../constants/tokenConfig'; +import { Dialog } from '../Dialog'; +import { PoolListItem } from './PoolListItem'; -interface PoolSelectorModalProps extends PoolListProps { +interface PoolSelectorModalProps extends PoolListProps { isLoading?: boolean; onClose: () => void; open: boolean; } -export interface SelectorMode { - type: 'from' | 'to' | undefined; - swap: boolean; +interface PoolListProps { + definitions: { assetSymbol: string; type: T }[]; + onSelect: (tokenType: InputTokenType | OutputTokenType) => void; + selected: InputTokenType | OutputTokenType | undefined; } -interface PoolListProps { - mode: SelectorMode; - onSelect: (pool: TokenDetails) => void; - selected: - | { type: 'token'; tokenAddress: string | undefined } - | { type: 'backstopPool' } - | { type: 'swapPool'; poolAddress: string }; -} - -export function PoolSelectorModal({ selected, isLoading, onSelect, onClose, open, mode }: PoolSelectorModalProps) { - return ( - - - -

{'Select a token'}

-
- -
- {isLoading ? ( - - ) : ( - - )} -
-
-
+export function PoolSelectorModal({ + selected, + isLoading, + definitions, + onSelect, + onClose, + open, +}: PoolSelectorModalProps) { + const content = isLoading ? ( + + ) : ( + ); -} - -function PoolList({ onSelect, selected, mode }: PoolListProps) { - const [filter, setFilter] = useState(); - - const poolList = useMemo(() => { - const poolList: TokenDetails[] = []; - (Object.keys(TOKEN_CONFIG) as TokenType[]).forEach((token) => { - // special case rules - // do not allow non-offramp tokens in the to field, - if (mode.type === 'to' && mode.swap && !TOKEN_CONFIG[token].isOfframp) return; - - // only allow USDC asset code from otherChain property - if ( - mode.type === 'from' && - mode.swap && - TOKEN_CONFIG[token].assetCode !== 'USDC' && - TOKEN_CONFIG[token].isPolygonChain !== true - ) - return; - // Do not allow non offrampable tokens in the from field if no swap - if (mode.type === 'from' && !mode.swap && !TOKEN_CONFIG[token].isOfframp) return; - - poolList.push(TOKEN_CONFIG[token]); - }); - - return poolList; - }, [mode]); + return ; +} +function PoolList({ onSelect, definitions, selected }: PoolListProps) { + const [_, setFilter] = useState(); return (
) => setFilter(ev.currentTarget.value)} placeholder="Find by name or address" />
- {poolList?.map((TokenDetails) => { - const { assetCode } = TokenDetails; - let isSelected; - switch (selected.type) { - case 'token': - isSelected = selected.tokenAddress === assetCode; - break; - } - + {definitions.map(({ assetSymbol, type }) => { + let isSelected = selected === assetSymbol; return ( - + ); })}
); } - -const ModalCloseButton = (props: ButtonProps): JSX.Element | null => ( - -); - -export default ModalCloseButton; diff --git a/src/components/InputKeys/To.tsx b/src/components/InputKeys/To.tsx deleted file mode 100644 index 12ac0988..00000000 --- a/src/components/InputKeys/To.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { ArrowPathRoundedSquareIcon, ChevronDownIcon } from '@heroicons/react/20/solid'; -import { useEffect } from 'preact/compat'; -import { Button } from 'react-daisyui'; -import { useFormContext } from 'react-hook-form'; -import Big from 'big.js'; - -import pendulumIcon from '../../assets/pendulum-icon.svg'; -import { NumberLoader, TokenBalance } from '../Nabla/TokenBalance'; -import { Skeleton } from '../Skeleton'; -import { SwapFormValues } from '../Nabla/schema'; -import { UseTokenOutAmountResult } from '../../hooks/nabla/useTokenAmountOut'; -import { useBoolean } from '../../hooks/useBoolean'; -import { TokenDetails } from '../../constants/tokenConfig'; -import { BalanceInfo } from '../Nabla/BalanceState'; - -export interface ToProps { - tokenId: string; - onOpenSelector: () => void; - fromToken: TokenDetails | undefined; - toToken: TokenDetails | undefined; - toAmountQuote: UseTokenOutAmountResult; - fromAmount: Big | undefined; -} - -export function To({ - tokenId, - fromToken, - toToken, - onOpenSelector, - toAmountQuote, - fromAmount, -}: ToProps): JSX.Element | null { - const toTokenBalance = undefined; - - // replace with use state - const [isOpen, { toggle }] = useBoolean(true); - const { setValue } = useFormContext(); - - useEffect(() => { - setValue('toAmount', toAmountQuote.data?.amountOut.preciseString ?? '0', { - shouldDirty: true, - shouldTouch: true, - shouldValidate: true, - }); - }, [toAmountQuote.data?.amountOut.preciseString, setValue]); - - return ( -
-
-
- {toAmountQuote.isLoading ? ( - - ) : toAmountQuote.data !== undefined && toAmountQuote.data !== null ? ( - `${toAmountQuote.data.amountOut.approximateStrings.atLeast4Decimals}` - ) : fromAmount !== undefined && fromAmount.gt(0) ? ( - - ) : ( - <>0 - )} -
- -
-
- {/*
{toToken ? : '$ -'}
*/} -
- Your balance:{' '} - {toTokenBalance ? : '0'} -
-
-
-
-
-
- {fromToken !== undefined && - toToken !== undefined && - !toAmountQuote.isLoading && - toAmountQuote.data != undefined ? ( - <>{`1 ${fromToken.assetCode} = ${toAmountQuote.data.effectiveExchangeRate} ${toToken.assetCode}`} - ) : ( - `-` - )} -
-
-
- -
-
-
-
-
-
Expected Output:
- {toAmountQuote.data !== undefined ? ( -
- - {toAmountQuote.data?.amountOut.approximateStrings.atLeast4Decimals} {toToken?.assetCode || ''} - -
- ) : ( -
N/A
- )} -
-
-
Swap fee:
-
- {toAmountQuote.data != undefined ? toAmountQuote.data.swapFee.approximateStrings.atLeast2Decimals : ''}{' '} - {toToken?.assetCode || ''} -
-
-
-
-
- ); -} diff --git a/src/components/InputKeys/index.tsx b/src/components/InputKeys/index.tsx deleted file mode 100644 index 280fc14c..00000000 --- a/src/components/InputKeys/index.tsx +++ /dev/null @@ -1,259 +0,0 @@ -import React, { useState, useEffect, useCallback } from 'react'; -import { ConnectButton } from '@rainbow-me/rainbowkit'; -import { getApiManagerInstance } from '../../services/polkadot/polkadotApi'; -import { useAccountBalance } from '../Nabla/BalanceState'; -import { TOKEN_CONFIG, TokenType } from '../../constants/tokenConfig'; -import { useTokenOutAmount } from '../../hooks/nabla/useTokenAmountOut'; -import { ApiPromise } from '../../services/polkadot/polkadotApi'; -import { Button, Card } from 'react-daisyui'; -import { Tabs } from 'react-daisyui'; -import { From } from './From'; -import { PoolSelectorModal } from './SelectionModal'; -import { FormProvider } from 'react-hook-form'; -import { To } from './To'; -import { useSwapForm } from '../Nabla/useSwapForm'; -import { toBigNumber } from '../../helpers/parseNumbers'; -import { Skeleton } from '../Skeleton'; -import { config } from '../../config'; -import Big from 'big.js'; -import { ExecutionInput } from '../../pages/landing'; -import { useAccount, useSignMessage } from 'wagmi'; - -const { RadioTab } = Tabs; - -interface InputBoxProps { - onSubmit: (input: ExecutionInput) => void; - dAppName: string; -} - -export interface SwapSettings { - from: string; - to: string; -} - -export interface SwapOptions { - assetIn: string; - minAmountOut: Big; -} - -function Loader() { - return ( -
- -
- ); -} - -const InputBox: React.FC = ({ onSubmit, dAppName }) => { - const [isSubmitted, setIsSubmitted] = useState(false); - const { address } = useAccount(); - const { signMessage } = useSignMessage(); - const [api, setApi] = useState(null); - const { balance, isBalanceLoading } = useAccountBalance(address); - - useEffect(() => { - const initializeApiManager = async () => { - const manager = await getApiManagerInstance(); - const { api, ss58Format } = await manager.getApiComponents(); - setApi(api); - }; - - initializeApiManager().catch(console.error); - }, []); - - const wantsSwap = true; - - const { - tokensModal: [modalType, setModalType], - onFromChange, - onToChange, - form, - fromAmount, - fromAmountString, - fromToken, - toToken, - from, - to, - } = useSwapForm(); - - const tokenOutData = useTokenOutAmount({ - wantsSwap, - api: api, - fromAmountString, - fromToken: from, - toToken: to, - maximumFromAmount: undefined, - xcmFees: config.xcm.fees, - slippageBasisPoints: config.swap.slippageBasisPoints, - form, - }); - - const { - formState: { errors }, - } = form; - - const formErrorMessage = errors.fromAmount?.message ?? errors.root?.message; - - const handleSubmit = async () => { - if (fromAmount === undefined || fromAmount.eq(0)) { - alert('Please enter an amount to offramp.'); - return; - } - - if (tokenOutData.data === null) return; - - let assetToOfframp; - let swapOptions: SwapOptions | undefined; - if (wantsSwap) { - // ensure the swap was calculated and no errors were found - if (inputHasErrors || tokenOutData.isLoading || tokenOutData.data === undefined) { - return; - } - - swapOptions = { - assetIn: from, - minAmountOut: tokenOutData.data.amountOut.preciseBigDecimal, - }; - assetToOfframp = to as TokenType; - } else { - assetToOfframp = from as TokenType; - } - - if (!address) { - alert('Please connect to a wallet first.'); - return; - } - - // check balance of the asset used to offramp directly or to pay for the swap - if (balance.preciseBigDecimal.lt(fromAmount)) { - alert(`Insufficient balance to offramp. Current balance is ${balance.approximateNumber} ${from.toUpperCase()}.`); - return; - } - - // If swap will happen, check the minimum comparing to the minimum expected swap - const minWithdrawalAmountBigNumber = toBigNumber( - TOKEN_CONFIG[assetToOfframp as TokenType].minWithdrawalAmount!, - TOKEN_CONFIG[assetToOfframp as TokenType].decimals, - ); - - if ( - wantsSwap && - from && - tokenOutData.data !== undefined && - minWithdrawalAmountBigNumber.gt(tokenOutData.data.amountOut.preciseBigDecimal) - ) { - alert(`Insufficient balance to offramp. Minimum withdrawal amount for ${assetToOfframp} is not met.`); - return; - } - - setIsSubmitted(true); - console.log( - 'submitting offramp', - '\n', - 'user address: ', - address, - '\n', - 'wants swap: ', - wantsSwap, - '\n', - 'asset to offramp: ', - assetToOfframp, - '\n', - 'amount in: ', - fromAmount, - '\n', - 'asset in: ', - from, - '\n', - 'asset out: ', - to, - '\n', - 'initial desired: ', - tokenOutData.data?.amountOut.approximateNumber, - ); - - onSubmit({ assetToOfframp, amountIn: fromAmount, swapOptions }); - }; - - // we don't propagate errors if wants swap is not defined - const inputHasErrors = wantsSwap && formErrorMessage !== undefined; - - return ( -
-
- setModalType(undefined)} - isLoading={false} - /> -
- -
- {!isSubmitted && ( -
-
    -
  • Do not close this window until the process is completed.
  • -
  • This is a non-custodial prototype, please use at your own risk.
  • -
-
- )} - -
- - Offramp Asset - -
- -
- {api === null || isBalanceLoading ? ( - - ) : wantsSwap ? ( - - setModalType('from')} - inputHasError={inputHasErrors} - form={form} - fromFormFieldName="fromAmount" - tokenBalance={balance} - /> -
{formErrorMessage !== undefined &&

{formErrorMessage}

}
-
- setModalType('to')} - fromAmount={fromAmount} - /> -
- ) : null} -
-
- - {!(from === '') && !isSubmitted && address ? ( - - ) : null} - {isSubmitted && ( -
- Offramp started for asset - {to && wantsSwap ? to.toUpperCase() : from?.toUpperCase()} -
- )} -
-
- ); -}; - -export default InputBox; diff --git a/src/components/LabeledInput/index.tsx b/src/components/LabeledInput/index.tsx new file mode 100644 index 00000000..19800d1b --- /dev/null +++ b/src/components/LabeledInput/index.tsx @@ -0,0 +1,13 @@ +import { FC } from 'preact/compat'; + +interface LabeledInputProps { + label: string; + Input: ReactNode; +} + +export const LabeledInput: FC = ({ label, Input }) => ( + +); diff --git a/src/components/Nabla/BalanceState.tsx b/src/components/Nabla/BalanceState.tsx deleted file mode 100644 index ed6acaff..00000000 --- a/src/components/Nabla/BalanceState.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { useEffect, useState } from 'react'; -import { toBigNumber } from '../../helpers/parseNumbers'; -import { getApiManagerInstance } from '../../services/polkadot/polkadotApi'; -import { TOKEN_CONFIG } from '../../constants/tokenConfig'; -import { parseContractBalanceResponse } from '../../helpers/contracts'; -import { ContractBalance } from '../../helpers/contracts'; -import { useReadContract } from 'wagmi'; -import BigNumber from 'big.js'; -import erc20ABI from '../../contracts/ERC20'; - -export interface BalanceInfo extends ContractBalance { - canWithdraw: boolean; -} - -export interface UseAccountBalanceResponse { - balance: BalanceInfo; - isBalanceLoading: boolean; - balanceError?: Error; -} -const zeroBalance = { - ...parseContractBalanceResponse(6, BigInt(0)), - canWithdraw: false, -}; - -export const useAccountBalance = (address?: string): UseAccountBalanceResponse => { - const [balanceParsed, setBalance] = useState(zeroBalance); - const [isBalanceLoading, setIsLoading] = useState(true); - const [balanceError, setError] = useState(); - - const { data: balance } = useReadContract({ - abi: erc20ABI, - address: TOKEN_CONFIG.usdc.erc20AddressNativeChain as `0x${string}`, - functionName: 'balanceOf', - args: [address], - }); - - useEffect(() => { - const fetchBalances = async () => { - if (!address) { - setBalance({ - ...zeroBalance, - canWithdraw: false, - }); - return; - } - try { - const rawBalance = balance as bigint; - const contractBalance = parseContractBalanceResponse(6, rawBalance); - - // We don't need this, now. Unless we wan't to support also offramp from native polygon chain. - // otherwise, the minimum is irrelevant. - const minWithdrawalAmount = toBigNumber(100, 0); - const canWithdraw = contractBalance.rawBalance.gte(minWithdrawalAmount); - - const balancePolygonAsset = { - ...contractBalance, - canWithdraw, - }; - - setBalance(balancePolygonAsset); - } catch (err) { - setError(err as Error); - } finally { - setIsLoading(false); - } - }; - - fetchBalances(); - }, [address, balance]); - - return { - balance: balanceParsed, - isBalanceLoading, - balanceError, - }; -}; diff --git a/src/components/Nabla/NumberInput.tsx b/src/components/Nabla/NumberInput.tsx deleted file mode 100644 index 67b58ed1..00000000 --- a/src/components/Nabla/NumberInput.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { HTMLAttributes, useCallback } from 'preact/compat'; -import { FieldPath, FieldValues, useFormContext } from 'react-hook-form'; - -export interface NumberInputProps extends HTMLAttributes { - registerName: FieldPath; - disabled: boolean; -} - -// Hack required for Preact compatibility with react-hook-form -export function NumberInput(attributes: NumberInputProps) { - const { register } = useFormContext(); - - const { registerName, disabled } = attributes; - - const registerAttributes: HTMLAttributes = register(registerName); - const { onChange } = registerAttributes; - const attributesWithoutOnChange = { ...registerAttributes }; - delete attributesWithoutOnChange.onChange; - - const onInput = useCallback( - (e: React.ChangeEvent) => { - let number = e.currentTarget.value.replace(/[^0-9.]/g, ''); - const periodIndex = number.indexOf('.'); - if (periodIndex > -1) { - number = number.slice(0, periodIndex + 1) + number.slice(periodIndex + 1).replace(/\./g, ''); - } - - e.currentTarget.value = number; - onChange?.(e); - }, - [onChange], - ); - - return ( - - ); -} diff --git a/src/components/Nabla/TokenBalance.tsx b/src/components/Nabla/TokenBalance.tsx deleted file mode 100644 index 7e59e76f..00000000 --- a/src/components/Nabla/TokenBalance.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Skeleton } from '../Skeleton'; -import { BalanceInfo } from './BalanceState'; - -export function NumberLoader() { - return 10000; -} - -export interface TokenBalancProps { - query: BalanceInfo | undefined; - symbol: string | undefined; - significantDecimals?: 2 | 4; -} - -export function TokenBalance({ query, symbol, significantDecimals }: TokenBalancProps): JSX.Element | null { - if (!query) { - return ; - } - - // if (error || !data) { - // return N/A; - // } - - const approximateString = - significantDecimals === 4 ? query.approximateStrings.atLeast4Decimals : query.approximateStrings.atLeast2Decimals; - return ( - - {approximateString} {symbol ? symbol : null} - - ); -} diff --git a/src/components/Nabla/schema.tsx b/src/components/Nabla/schema.tsx index 671363b2..4a769242 100644 --- a/src/components/Nabla/schema.tsx +++ b/src/components/Nabla/schema.tsx @@ -1,17 +1,33 @@ import * as Yup from 'yup'; +import { InputTokenType, OutputTokenType } from '../../constants/tokenConfig'; export type SwapFormValues = { - from: string; + from: InputTokenType; fromAmount: string; - to: string; + to: OutputTokenType; toAmount: string; + slippage: number | undefined; + deadline: number; + bankAccount: string; + taxNumber: string; +}; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +const transformNumber = (value: any, originalValue: any) => { + if (!originalValue) return 0; + if (typeof originalValue === 'string' && originalValue !== '') value = Number(originalValue) ?? 0; + return value; }; const schema = Yup.object().shape({ - from: Yup.string().min(5).required(), + from: Yup.string().required(), fromAmount: Yup.string().required(), - to: Yup.string().min(5).required(), + to: Yup.string().required(), toAmount: Yup.string().required(), + slippage: Yup.number().nullable().transform(transformNumber), + deadline: Yup.number().nullable().transform(transformNumber), + bankAccount: Yup.string().required(), + taxNumber: Yup.string().required(), }); export default schema; diff --git a/src/components/Nabla/useSwapForm.tsx b/src/components/Nabla/useSwapForm.tsx index 21747451..d17926aa 100644 --- a/src/components/Nabla/useSwapForm.tsx +++ b/src/components/Nabla/useSwapForm.tsx @@ -1,14 +1,18 @@ -import { useState, useCallback, useMemo } from 'react'; import Big from 'big.js'; import { Resolver, useForm, useWatch } from 'react-hook-form'; +import { useState, useCallback, useMemo } from 'preact/compat'; import { yupResolver } from '@hookform/resolvers/yup'; -import { TOKEN_CONFIG, TokenDetails, TokenType } from '../../constants/tokenConfig'; -import schema, { SwapFormValues } from './schema'; -import { storageService } from '../../services/localStorage'; +import { INPUT_TOKEN_CONFIG, InputTokenType, OUTPUT_TOKEN_CONFIG, OutputTokenType } from '../../constants/tokenConfig'; import { storageKeys } from '../../constants/localStorage'; import { debounce } from '../../helpers/function'; -import { SwapSettings } from '../InputKeys'; +import schema, { SwapFormValues } from './schema'; +import { storageService } from '../../services/storage/local'; + +interface SwapSettings { + from: string; + to: string; +} const storageSet = debounce(storageService.set, 1000); const setStorageForSwapSettings = storageSet.bind(null, storageKeys.SWAP_SETTINGS); @@ -20,8 +24,10 @@ export const useSwapForm = () => { const initialState = useMemo(() => { const storageValues = storageService.getParsed(storageKeys.SWAP_SETTINGS); return { - from: storageValues?.from ?? 'usdc', - to: storageValues?.to ?? '', + from: (storageValues?.from ?? 'usdc') as InputTokenType, + to: (storageValues?.to ?? 'brl') as OutputTokenType, + taxNumber: '', + bankAccount: '', }; }, []); @@ -34,47 +40,38 @@ export const useSwapForm = () => { const from = useWatch({ control, name: 'from' }); const to = useWatch({ control, name: 'to' }); - const fromToken = from ? TOKEN_CONFIG[from as TokenType] : undefined; - const toToken = to ? TOKEN_CONFIG[to as TokenType] : undefined; + const fromToken = from ? INPUT_TOKEN_CONFIG[from] : undefined; + const toToken = to ? OUTPUT_TOKEN_CONFIG[to] : undefined; const onFromChange = useCallback( - (a: TokenDetails) => { + (tokenKey: string) => { const prev = getValues(); - const tokenKey = Object.keys(TOKEN_CONFIG).find( - (key) => TOKEN_CONFIG[key as TokenType]!.assetCode === a.assetCode, - ); - if (!tokenKey) return; const updated = { from: tokenKey, - to: prev?.to === tokenKey ? prev?.from : prev?.to, + to: prev?.to, }; - if (updated.to && prev?.to === tokenKey) setValue('to', updated.to); setStorageForSwapSettings(updated); - setValue('from', tokenKey); + setValue('from', tokenKey as InputTokenType); setTokenModal(undefined); }, - [getValues, setValue, setTokenModal], + [form, setValue, setTokenModal], ); const onToChange = useCallback( - (a: TokenDetails) => { + (tokenKey: string) => { const prev = getValues(); - const tokenKey = Object.keys(TOKEN_CONFIG).find( - (key) => TOKEN_CONFIG[key as TokenType]!.assetCode === a.assetCode, - ); if (!tokenKey) return; const updated = { to: tokenKey, - from: prev?.from === tokenKey ? prev?.to : prev?.from, + from: prev?.from, }; - if (updated.from && prev?.from !== updated.from) setValue('from', updated.from); setStorageForSwapSettings(updated); - setValue('to', tokenKey); + setValue('to', tokenKey as OutputTokenType); setTokenModal(undefined); }, @@ -105,5 +102,6 @@ export const useSwapForm = () => { fromAmountString, fromToken, toToken, + reset: form.reset, }; }; diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx new file mode 100644 index 00000000..c9e2e1ae --- /dev/null +++ b/src/components/Navbar/index.tsx @@ -0,0 +1,109 @@ +import { Bars4Icon, XMarkIcon } from '@heroicons/react/20/solid'; +import { motion, AnimatePresence } from 'framer-motion'; + +import whiteLogo from '../../assets/logo/white.png'; +import { ConnectWallet } from '../buttons/ConnectWallet'; +import { useState } from 'preact/hooks'; +import { FC } from 'preact/compat'; + +const links = [ + { title: 'About', href: '/about' }, + { title: 'Ecosystem', href: '/ecosystem' }, + { title: 'Help', href: '/help' }, +]; + +interface MobileMenuProps { + onClick: () => void; +} + +const MobileMenu: FC = ({ onClick }) => ( + +); + +const menuVariants = { + hidden: { + y: '-100vh', + }, + visible: { + y: 0, + transition: { + type: 'tween', + duration: 0.3, + }, + }, + exit: { + y: '-100vh', + transition: { + type: 'tween', + duration: 0.3, + delay: 0.3, + }, + }, +}; +interface MobileMenuListProps { + showMenu: boolean; + closeMenu: () => void; +} + +const MobileMenuList: FC = ({ showMenu, closeMenu }) => ( + + {showMenu && ( + + + + Vortex Logo + + )} + +); + +const Links = () => ( + +); + +export const Navbar = () => { + const [showMenu, setShowMenu] = useState(false); + + return ( +
+
+ Vortex Logo + +
+
+ + setShowMenu(true)} /> + setShowMenu(false)} /> +
+
+ ); +}; diff --git a/src/components/NumericInput/NumericInput.test.tsx b/src/components/NumericInput/NumericInput.test.tsx new file mode 100644 index 00000000..c62c18ce --- /dev/null +++ b/src/components/NumericInput/NumericInput.test.tsx @@ -0,0 +1,110 @@ +import { h } from 'preact'; +import { UseFormRegisterReturn } from 'react-hook-form'; +import { render } from '@testing-library/preact'; +import userEvent from '@testing-library/user-event'; +import { NumericInput } from '.'; + +const mockRegister: UseFormRegisterReturn = { + name: 'testInput', + onChange: jest.fn(), + onBlur: jest.fn(), + ref: jest.fn(), +}; + +describe('NumericInput Component', () => { + it('should render the component', () => { + const { getByPlaceholderText } = render(); + const inputElement = getByPlaceholderText('0.0'); + expect(inputElement).toBeInTheDocument(); + }); + + it('should allow numeric input', async () => { + const { getByPlaceholderText } = render(); + const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; + + await userEvent.type(inputElement, '1234567890'); + expect(inputElement.value).toBe('1234567890'); + }); + + it('should prevent non-numeric input', async () => { + const { getByPlaceholderText } = render(); + const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; + + await userEvent.type(inputElement, 'qwertyuiopasdfghjklzxcvbnm'); + expect(inputElement.value).toBe(''); + }); + + it('should prevent multiple decimal points', async () => { + const { getByPlaceholderText } = render(); + const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; + + await userEvent.type(inputElement, '1.1.1,2.3,4'); + expect(inputElement.value).toBe('1.11234'); + }); + + it('should replace comma with period', async () => { + const { getByPlaceholderText } = render(); + const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; + + await userEvent.type(inputElement, '1,1'); + expect(inputElement.value).toBe('1.1'); + }); + + it('should work with readOnly prop', () => { + const { getByPlaceholderText } = render(); + const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; + + expect(inputElement).toHaveAttribute('readOnly'); + }); + + it('should apply additional styles', () => { + const { getByPlaceholderText } = render(); + const inputElement = getByPlaceholderText('0.0'); + + expect(inputElement).toHaveClass('extra-style'); + }); + + it('should handle leading zeros correctly', async () => { + const { getByPlaceholderText } = render(); + const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; + + await userEvent.type(inputElement, '007'); + expect(inputElement.value).toBe('007'); + }); + + it('should allow backspace and delete', async () => { + const { getByPlaceholderText } = render(); + const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; + + await userEvent.type(inputElement, '123'); + await userEvent.type(inputElement, '{backspace}'); + expect(inputElement.value).toBe('12'); + + await userEvent.type(inputElement, '{delete}'); + expect(inputElement.value).toBe('12'); + }); + + it('should not allow negative numbers', async () => { + const { getByPlaceholderText } = render(); + const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; + + await userEvent.type(inputElement, '-123'); + expect(inputElement.value).toBe('123'); + }); + + it('should not allow more decimals than maxDecimals', async () => { + const { getByPlaceholderText } = render(); + const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; + + await userEvent.type(inputElement, '123.45479187249871298774985'); + expect(inputElement.value).toBe('123.45'); + }); + + it('should not allow more decimals than default maxDecimals', async () => { + const { getByPlaceholderText } = render(); + const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; + + await userEvent.type(inputElement, '123.4567890123456789abcgdehyu0123456.2746472.93.2.7.3.5.3'); + expect(inputElement.value).toBe('123.456789012343'); + }); +}); diff --git a/src/components/NumericInput/index.tsx b/src/components/NumericInput/index.tsx new file mode 100644 index 00000000..7e80615e --- /dev/null +++ b/src/components/NumericInput/index.tsx @@ -0,0 +1,83 @@ +import { Input } from 'react-daisyui'; +import { UseFormRegisterReturn } from 'react-hook-form'; + +export function exceedsMaxDecimals(value: unknown, maxDecimals: number) { + if (value === undefined || value === null) return true; + const decimalPlaces = value.toString().split('.')[1]; + return decimalPlaces ? decimalPlaces.length > maxDecimals : false; +} + +interface NumericInputProps { + register: UseFormRegisterReturn; + readOnly?: boolean; + additionalStyle?: string; + maxDecimals?: number; + defaultValue?: string; + autoFocus?: boolean; + disabled?: boolean; + disableStyles?: boolean; +} + +function isValidNumericInput(value: string): boolean { + return /^[0-9.,]*$/.test(value); +} + +function alreadyHasDecimal(e: KeyboardEvent) { + const decimalChars = ['.', ',']; + + // In the onInput event, "," is replaced by ".", so we check if the e.target.value already contains a "." + return decimalChars.some((char) => e.key === char && e.target && (e.target as HTMLInputElement).value.includes('.')); +} + +function handleOnInput(e: KeyboardEvent): void { + const target = e.target as HTMLInputElement; + target.value = target.value.replace(/,/g, '.'); +} + +function handleOnKeyPress(e: KeyboardEvent, maxDecimals: number): void { + if (!isValidNumericInput(e.key) || alreadyHasDecimal(e)) { + e.preventDefault(); + } + const target = e.target as HTMLInputElement; + if (exceedsMaxDecimals(target.value, maxDecimals - 1)) { + target.value = target.value.slice(0, -1); + } +} + +export const NumericInput = ({ + register, + readOnly = false, + additionalStyle, + maxDecimals = 2, + defaultValue, + autoFocus, + disabled, + disableStyles = false, +}: NumericInputProps) => ( +
+ handleOnKeyPress(e, maxDecimals)} + onInput={handleOnInput} + pattern="^[0-9]*[.,]?[0-9]*$" + placeholder="0.0" + readOnly={readOnly} + spellcheck="false" + step="any" + type="text" + inputmode="decimal" + value={defaultValue} + autoFocus={autoFocus} + disabled={disabled} + {...register} + /> +
+); diff --git a/src/components/PublicKey.tsx b/src/components/PublicKey.tsx deleted file mode 100644 index d0f87dca..00000000 --- a/src/components/PublicKey.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { memo, useCallback } from 'preact/compat'; -import { useClipboard } from '../hooks/useClipboard'; -import { Button } from '@mui/material'; - -type Variant = 'full' | 'short' | 'shorter' | 'hexa'; - -function getDigitCounts(variant?: Variant) { - if (variant === 'short') { - return { - leading: 6, - trailing: 6, - }; - } else if (variant === 'hexa') { - return { - leading: 10, - trailing: 10, - }; - } else { - return { - leading: 4, - trailing: 4, - }; - } -} - -export function shortenName(name: string, intendedLength: number) { - if (name.length <= intendedLength) { - return name; - } else { - return ( - name.substr(0, intendedLength - 3).trim() + - '…' + - name - .substr(intendedLength - 3) - .substr(-3) - .trim() - ); - } -} - -interface PublicKeyProps { - publicKey: string; - variant?: Variant; - style?: React.CSSProperties; - className?: string; - showRaw?: boolean; -} - -// tslint:disable-next-line no-shadowed-variable -export const PublicKey = memo(function PublicKey(props: PublicKeyProps) { - const { variant = 'full', className } = props; - const digits = getDigitCounts(props.variant); - - const style: React.CSSProperties = { - userSelect: 'text', - WebkitUserSelect: 'text', - whiteSpace: variant !== 'full' ? 'pre' : undefined, - ...props.style, - }; - - return ( - - {props.variant === 'full' || !props.variant - ? props.publicKey - : props.publicKey.substr(0, digits.leading) + '…' + props.publicKey.substr(-digits.trailing)} - - ); -}); - -interface AddressProps { - publicKey: string; - variant?: Variant; - inline?: boolean; - style?: React.CSSProperties; - className?: string; - icon?: JSX.Element; - onClick?: () => void; - wrap?: boolean; -} - -// tslint:disable-next-line no-shadowed-variable -export const ClickableAddress = memo(function ClickableAddress(props: AddressProps) { - return ( - - ); -}); - -interface CopyableAddressProps extends AddressProps { - onClick?: () => void; -} - -// tslint:disable-next-line no-shadowed-variable -export const CopyableAddress = memo(function CopyableAddress(props: CopyableAddressProps) { - const { onClick } = props; - const clipboard = useClipboard(); - - const handleClick = useCallback(() => { - if (onClick) { - onClick(); - } - clipboard.copyToClipboard(props.publicKey); - }, [clipboard, onClick, props.publicKey]); - - return ; -}); diff --git a/src/components/PublicKey/index.tsx b/src/components/PublicKey/index.tsx deleted file mode 100644 index 3302d4b9..00000000 --- a/src/components/PublicKey/index.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { memo, useCallback } from 'preact/compat'; -import { useClipboard } from '../../hooks/useClipboard'; -import { Button } from '@mui/material'; - -type Variant = 'full' | 'short' | 'shorter' | 'hexa'; - -function getDigitCounts(variant?: Variant) { - if (variant === 'short') { - return { - leading: 6, - trailing: 6, - }; - } else if (variant === 'hexa') { - return { - leading: 10, - trailing: 10, - }; - } else { - return { - leading: 4, - trailing: 4, - }; - } -} - -export function shortenName(name: string, intendedLength: number) { - if (name.length <= intendedLength) { - return name; - } else { - return ( - name.substr(0, intendedLength - 3).trim() + - '…' + - name - .substr(intendedLength - 3) - .substr(-3) - .trim() - ); - } -} - -interface PublicKeyProps { - publicKey: string; - variant?: Variant; - style?: React.CSSProperties; - className?: string; - showRaw?: boolean; -} - -// tslint:disable-next-line no-shadowed-variable -export const PublicKey = memo(function PublicKey(props: PublicKeyProps) { - const { variant = 'full', className } = props; - const digits = getDigitCounts(props.variant); - - const style: React.CSSProperties = { - userSelect: 'text', - WebkitUserSelect: 'text', - whiteSpace: variant !== 'full' ? 'pre' : undefined, - ...props.style, - }; - - return ( - - {props.variant === 'full' || !props.variant - ? props.publicKey - : props.publicKey.substr(0, digits.leading) + '…' + props.publicKey.substr(-digits.trailing)} - - ); -}); - -interface AddressProps { - publicKey: string; - variant?: Variant; - inline?: boolean; - style?: React.CSSProperties; - className?: string; - icon?: JSX.Element; - onClick?: () => void; - wrap?: boolean; -} - -// tslint:disable-next-line no-shadowed-variable -export const ClickableAddress = memo(function ClickableAddress(props: AddressProps) { - return ( - - ); -}); - -interface CopyableAddressProps extends AddressProps { - onClick?: () => void; -} - -// tslint:disable-next-line no-shadowed-variable -export const CopyableAddress = memo(function CopyableAddress(props: CopyableAddressProps) { - const { onClick } = props; - const clipboard = useClipboard(); - - const handleClick = useCallback(() => { - if (onClick) { - onClick(); - } - clipboard.copyToClipboard(props.publicKey); - }, [clipboard, onClick, props.publicKey]); - - return ; -}); diff --git a/src/components/Sep24Component.tsx b/src/components/Sep24Component.tsx deleted file mode 100644 index d31803c8..00000000 --- a/src/components/Sep24Component.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { IAnchorSessionParams, Sep24Result } from '../services/anchor'; -import { sep24First, sep24Second } from '../services/anchor'; -import { EventStatus } from './GenericEvent'; -import { Button } from 'react-daisyui'; - -interface Sep24Props { - sessionParams: IAnchorSessionParams | null; - onSep24Complete: (sep24Reslt: Sep24Result) => void; - addEvent: (message: string, status: EventStatus) => void; -} - -const Sep24: React.FC = ({ sessionParams, onSep24Complete, addEvent }) => { - const [sep24Url, setSep24Url] = useState(undefined); - - useEffect(() => { - if (sessionParams) { - (async () => { - const firstResponse = await sep24First(sessionParams, addEvent); - setSep24Url(firstResponse.url); - const secondResponse = await sep24Second(firstResponse, sessionParams); - onSep24Complete(secondResponse); - })(); - - addEvent('Waiting for confirmation from Anchor', EventStatus.Waiting); - } - }, [sessionParams]); - - return ( -
- -
- ); -}; - -export default Sep24; diff --git a/src/components/Skeleton/index.tsx b/src/components/Skeleton/index.tsx deleted file mode 100644 index 8e23de79..00000000 --- a/src/components/Skeleton/index.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { CSSProperties, HTMLAttributes } from 'preact/compat'; - -export type SkeletonProps = { - isLoading?: boolean; - style?: CSSProperties; -} & Omit, 'style'>; - -export const Skeleton = ({ className, isLoading, children, ...rest }: SkeletonProps) => - isLoading === false ? ( - <>{children} - ) : ( -
-
{children}
-
- ); diff --git a/src/components/TextInput/index.tsx b/src/components/TextInput/index.tsx new file mode 100644 index 00000000..ec5d3a07 --- /dev/null +++ b/src/components/TextInput/index.tsx @@ -0,0 +1,43 @@ +import { Input } from 'react-daisyui'; +import { UseFormRegisterReturn } from 'react-hook-form'; + +interface NumericInputProps { + register: UseFormRegisterReturn; + readOnly?: boolean; + additionalStyle?: string; + autoFocus?: boolean; + disabled?: boolean; + error?: boolean; + placeholder?: string; +} + +export const TextInput = ({ + register, + readOnly = false, + additionalStyle, + autoFocus, + disabled, + error, + placeholder, +}: NumericInputProps) => ( +
+ +
+); diff --git a/src/components/buttons/AssetButton/index.tsx b/src/components/buttons/AssetButton/index.tsx new file mode 100644 index 00000000..5275734b --- /dev/null +++ b/src/components/buttons/AssetButton/index.tsx @@ -0,0 +1,25 @@ +import { FC } from 'preact/compat'; +import { InputTokenType, OutputTokenType } from '../../../constants/tokenConfig'; +import { useGetIcon } from '../../../hooks/useGetIcon'; + +interface AssetButtonProps { + tokenType?: InputTokenType | OutputTokenType; + tokenSymbol?: string; + onClick: () => void; +} +export function AssetButton({ tokenType, tokenSymbol, onClick }: AssetButtonProps) { + const icon = useGetIcon(tokenType); + + return ( + + ); +} diff --git a/src/components/buttons/CloseButton/index.tsx b/src/components/buttons/CloseButton/index.tsx new file mode 100644 index 00000000..922742d0 --- /dev/null +++ b/src/components/buttons/CloseButton/index.tsx @@ -0,0 +1,19 @@ +import { Button } from 'react-daisyui'; +import { ButtonProps } from 'react-daisyui/dist/Button/Button'; +import { CloseIcon } from '../../../assets/CloseIcon'; + +export const CloseButton = (props: ButtonProps) => ( + +); diff --git a/src/components/buttons/ConnectWallet/index.tsx b/src/components/buttons/ConnectWallet/index.tsx new file mode 100644 index 00000000..f17aa5ad --- /dev/null +++ b/src/components/buttons/ConnectWallet/index.tsx @@ -0,0 +1,67 @@ +import { PlayCircleIcon } from '@heroicons/react/20/solid'; +import AccountBalanceWalletOutlinedIcon from '@mui/icons-material/AccountBalanceWalletOutlined'; +import { ConnectButton } from '@rainbow-me/rainbowkit'; + +export const ConnectWallet = () => ( + + {({ account, chain, openAccountModal, openChainModal, openConnectModal, authenticationStatus, mounted }) => { + const ready = mounted && authenticationStatus !== 'loading'; + const connected = + ready && account && chain && (!authenticationStatus || authenticationStatus === 'authenticated'); + + return ( +
+ {(() => { + if (!connected) { + return ( + + ); + } + + if (chain.unsupported) { + return ( + + ); + } + + return ( + <> + + + ); + })()} +
+ ); + }} +
+); diff --git a/src/components/buttons/SwapSubmitButton/index.tsx b/src/components/buttons/SwapSubmitButton/index.tsx new file mode 100644 index 00000000..909b2271 --- /dev/null +++ b/src/components/buttons/SwapSubmitButton/index.tsx @@ -0,0 +1,40 @@ +import { ConnectButton } from '@rainbow-me/rainbowkit'; +import { FC } from 'preact/compat'; + +interface SwapSubmitButtonProps { + text: string; + disabled: boolean; +} +export const SwapSubmitButton: FC = ({ text, disabled }) => ( + + {({ account, chain, openConnectModal, authenticationStatus, mounted }) => { + const ready = mounted && authenticationStatus !== 'loading'; + const connected = + ready && account && chain && (!authenticationStatus || authenticationStatus === 'authenticated'); + + return ( +
+ {(() => { + if (!connected) { + return ( + + ); + } + + return ( + + ); + })()} +
+ ); + }} +
+); diff --git a/src/config/index.ts b/src/config/index.ts index 888a1e66..d199ccf5 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -11,7 +11,7 @@ type TenantConfig = Record< } >; -export type Environment = 'development' | 'staging' | 'production'; +type Environment = 'development' | 'staging' | 'production'; const nodeEnv = process.env.NODE_ENV as Environment; const maybeSignerServiceUrl = import.meta.env.VITE_SIGNING_SERVICE_URL; const alchemyApiKey = import.meta.env.VITE_ALCHEMY_API_KEY; @@ -60,4 +60,8 @@ export const config = { url: 'wss://relay.walletconnect.com', projectId: '299fda67fbf3b60a31ba8695524534cd', }, + test: { + mockSep24: false, + overwriteMinimumTransferAmount: false, + }, }; diff --git a/src/constants/cache.ts b/src/constants/cache.ts index cde5ad98..74d7f235 100644 --- a/src/constants/cache.ts +++ b/src/constants/cache.ts @@ -16,7 +16,7 @@ export const cacheKeys = { nablaInstance: 'nablaInstance', }; -export type QueryOptions = Partial< +type QueryOptions = Partial< // eslint-disable-next-line @typescript-eslint/no-explicit-any Omit, 'queryKey' | 'queryFn'> >; diff --git a/src/constants/localStorage.ts b/src/constants/localStorage.ts index 303c67b1..6c22744b 100644 --- a/src/constants/localStorage.ts +++ b/src/constants/localStorage.ts @@ -2,4 +2,15 @@ export const storageKeys = { ACCOUNT: 'ACCOUNT', SWAP_SETTINGS: 'SWAP_SETTINGS_OFFRAMP', POOL_SETTINGS: 'POOL_SETTINGS', + OFFRAMP_STATUS: 'OFFRAMP_STATUS', + OFFRAMP_EXECUTION_INPUTS: 'OFFRAMP_EXECUTION_INPUTS', + PENDULUM_SEED: 'PENDULUM_SEED', + STELLAR_SEED: 'STELLAR_SEED', + SEP_RESULT: 'SEP24_RESULT', + ANCHOR_SESSION_PARAMS: 'ANCHOR_SESSION_PARAMS', + STELLAR_OPERATIONS: 'STELLAR_OPERATIONS', + TOKEN_BRIDGED_AMOUNT: 'TOKEN_BRIDGED_AMOUNT', + + // Internal squidrouter recovery states + SQUIDROUTER_RECOVERY_STATE: 'SQUIDROUTER_TRANSACTION_STATE', }; diff --git a/src/constants/tokenConfig.ts b/src/constants/tokenConfig.ts index 1c928c25..aa8ca4d9 100644 --- a/src/constants/tokenConfig.ts +++ b/src/constants/tokenConfig.ts @@ -1,83 +1,101 @@ import BrlIcon from '../assets/coins/BRL.png'; -import UsdtIcon from '../assets/coins/USDT.png'; -import EurcIcon from '../assets/coins/EURC.png'; import UsdcIcon from '../assets/coins/USDC.png'; +import EurcIcon from '../assets/coins/EURC.png'; -export interface TokenDetails { - currencyId: any; - isOfframp: boolean; +export interface InputTokenDetails { + assetSymbol: string; + erc20AddressSourceChain: `0x${string}`; + axelarEquivalent: { + pendulumErc20WrapperAddress: string; + pendulumCurrencyId: any; + pendulumAssetSymbol: string; + }; decimals: number; - erc20Address?: string; - assetCode: string; - assetIssuer?: string; - canSwapTo: string[]; - // optional depending if the asset is allowd to be offramped - tomlFileUrl?: string; - vaultAccountId?: string; - minWithdrawalAmount?: string; - assetCodeHex?: string; // Optional property icon: string; - isPolygonChain?: boolean; - erc20AddressNativeChain?: string; } -export type TokenType = 'brl' | 'eurc' | 'usdc'; -export type TokenConfig = Record; +export type InputTokenType = 'usdc'; + +export interface OutputTokenDetails { + tomlFileUrl: string; + decimals: number; + stellarAsset: { + code: { + hex: string; + string: string; + }; + issuer: { + hex: string; + stellarEncoding: string; + }; + }; + vaultAccountId: string; + minWithdrawalAmountRaw: string; + maxWithdrawalAmountRaw: string; + erc20WrapperAddress: string; + icon: string; +} +export const INPUT_TOKEN_CONFIG: Record = { + usdc: { + assetSymbol: 'USDC.e', + erc20AddressSourceChain: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', // USDC.e on Polygon + axelarEquivalent: { + pendulumErc20WrapperAddress: '6cXCaQeLQtYhyaQgMGaLcBakgfdgNiSoENW2LA2z8nLBcpSh', + pendulumCurrencyId: { XCM: 12 }, + pendulumAssetSymbol: 'USDC.axl', + }, + decimals: 6, + icon: UsdcIcon, + }, +}; -// Every asset specified in here must either be offrampable or be swapable to an offrampable asset -export const TOKEN_CONFIG: TokenConfig = { +export type OutputTokenType = 'brl' | 'eurc'; +export const OUTPUT_TOKEN_CONFIG: Record = { brl: { tomlFileUrl: 'https://ntokens.com/.well-known/stellar.toml', - isOfframp: true, decimals: 12, - currencyId: { - Stellar: { - AlphaNum4: { code: '0x42524c00', issuer: '0xeaac68d4d0e37b4c24c2536916e830735f032d0d6b2a1c8fca3bc5a25e083e3a' }, + stellarAsset: { + code: { + hex: '0x42524c00', + string: 'BRL', + }, + issuer: { + hex: '0xeaac68d4d0e37b4c24c2536916e830735f032d0d6b2a1c8fca3bc5a25e083e3a', + stellarEncoding: 'GDVKY2GU2DRXWTBEYJJWSFXIGBZV6AZNBVVSUHEPZI54LIS6BA7DVVSP', }, }, - assetCode: 'BRL', - canSwapTo: ['usdt', 'eurc'], - assetIssuer: 'GDVKY2GU2DRXWTBEYJJWSFXIGBZV6AZNBVVSUHEPZI54LIS6BA7DVVSP', vaultAccountId: '6g7fKQQZ9VfbBTQSaKBcATV4psApFra5EDwKLARFZCCVnSWS', - minWithdrawalAmount: '200000000000000', - assetCodeHex: '0x42524c00', - erc20Address: '6dZCR7KVmrcxBoUTcM3vUgpQagQAW2wg2izMrT3N4reftwW5', + minWithdrawalAmountRaw: '200000000000000', + maxWithdrawalAmountRaw: '10000000000000000', + erc20WrapperAddress: '6dZCR7KVmrcxBoUTcM3vUgpQagQAW2wg2izMrT3N4reftwW5', icon: BrlIcon, }, eurc: { tomlFileUrl: 'https://mykobo.co/.well-known/stellar.toml', - isOfframp: true, decimals: 12, - currencyId: { - Stellar: { - AlphaNum4: { code: 'EURC', issuer: '0x2112ee863867e4e219fe254c0918b00bc9ea400775bfc3ab4430971ce505877c' }, + stellarAsset: { + code: { + hex: '0x45555243', + string: 'EURC', + }, + issuer: { + hex: '0x2112ee863867e4e219fe254c0918b00bc9ea400775bfc3ab4430971ce505877c', + stellarEncoding: 'GAQRF3UGHBT6JYQZ7YSUYCIYWAF4T2SAA5237Q5LIQYJOHHFAWDXZ7NM', }, }, - canSwapTo: ['usdt', 'brl'], - assetCode: 'EURC', - assetIssuer: 'GAQRF3UGHBT6JYQZ7YSUYCIYWAF4T2SAA5237Q5LIQYJOHHFAWDXZ7NM', vaultAccountId: '6bsD97dS8ZyomMmp1DLCnCtx25oABtf19dypQKdZe6FBQXSm', - erc20Address: '6fA9DRKJ12oTXfSAU7ZZGZ9gEQ92YnyRXeJzW1wXekPzeXZC', - minWithdrawalAmount: '10000000000000', + erc20WrapperAddress: '6fA9DRKJ12oTXfSAU7ZZGZ9gEQ92YnyRXeJzW1wXekPzeXZC', + minWithdrawalAmountRaw: '10000000000000', + maxWithdrawalAmountRaw: '10000000000000000', icon: EurcIcon, }, - // We treat many of the properties of polygon token as the equivalent axl{X} one on Pendulum. - // we will receive - usdc: { - assetCode: 'USDC', - erc20AddressNativeChain: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', // USDC.e on Polygon - // erc20AddressNativeChain: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', // USDC on Polygon - // erc20Address is that of axlUSDC on pendulum - // this is done to provide the user with the expected exchange rate - erc20Address: '6cXCaQeLQtYhyaQgMGaLcBakgfdgNiSoENW2LA2z8nLBcpSh', - // Decimals should be consistent in BOTH CHAINS - decimals: 6, - // currency id of axlUSDC - currencyId: { XCM: 12 }, - isOfframp: false, - // whatever axlUSDC can be offramped to... - canSwapTo: ['eurc'], - icon: UsdcIcon, - isPolygonChain: true, - }, -} as const; +}; + +export function getPendulumCurrencyId(outputTokenType: OutputTokenType): any { + const { stellarAsset } = OUTPUT_TOKEN_CONFIG[outputTokenType]; + return { + Stellar: { + AlphaNum4: { code: stellarAsset.code.hex, issuer: stellarAsset.issuer.hex }, + }, + }; +} diff --git a/src/helpers/contracts.ts b/src/helpers/contracts.ts index 11339c26..779b848d 100644 --- a/src/helpers/contracts.ts +++ b/src/helpers/contracts.ts @@ -4,6 +4,7 @@ import { Limits } from '@pendulum-chain/api-solang'; import type { QueryKey, UseQueryOptions } from '@tanstack/react-query'; import type { ApiPromise } from '@polkadot/api'; import { ContractOptions } from '@polkadot/api-contract/types'; +import { roundDownToSignificantDecimals } from './parseNumbers'; const BIG_0 = new BigNumber('0'); @@ -86,10 +87,6 @@ export function parseContractBalanceResponse( }; } -function roundDownToSignificantDecimals(big: BigNumber, decimals: number) { - return big.prec(Math.max(0, big.e + 1) + decimals, 0); -} - export function stringifyBigWithSignificantDecimals(big: BigNumber, decimals: number) { const rounded = roundDownToSignificantDecimals(big, decimals); diff --git a/src/helpers/parseNumbers.ts b/src/helpers/parseNumbers.ts index db56cb3a..fbc83b41 100644 --- a/src/helpers/parseNumbers.ts +++ b/src/helpers/parseNumbers.ts @@ -1,4 +1,5 @@ import { u128 } from '@polkadot/types-codec'; +import Big from 'big.js'; import BigNumber from 'big.js'; // These are the decimals used for the native currency on the Amplitude network @@ -34,11 +35,6 @@ export const decimalToCustom = (value: BigNumber | number | string, decimals: nu return bigIntValue.mul(multiplier); }; -// Same as above, but handle a string decimal -export const stringDecimalToNative = (value: string) => { - return decimalToNative(stringDecimalToBN(value, ChainDecimals)); -}; - export const decimalToStellarNative = (value: BigNumber | number | string) => { let bigIntValue; try { @@ -50,37 +46,6 @@ export const decimalToStellarNative = (value: BigNumber | number | string) => { return bigIntValue.mul(multiplier); }; -// Same as above, but handle a string decimal -export const stringDecimalToStellarNative = (value: string) => { - return stringDecimalToBN(value, StellarDecimals); -}; - -// Convert a string decimal to a BigNumber -export const stringDecimalToBN = (value: string, chainDecimals: number) => { - let [whole, decimal] = value.split('.'); - decimal = decimal || '0'; - - //TODO this may not be needed now that we go back to big.js - // pad the decimal part - while (decimal.length < chainDecimals) { - decimal += '0'; - } - - // truncate the decimal part to max chain length digits - // and concatenate the whole and decimal parts - decimal = decimal.substring(0, chainDecimals); - const fullIntegerValue = whole + decimal; - - let bigIntValue; - try { - bigIntValue = new BigNumber(fullIntegerValue); - } catch (error) { - console.error('Error converting to BigNumber:', error); - bigIntValue = new BigNumber(0); - } - return bigIntValue; -}; - export const fixedPointToDecimal = (value: BigNumber | number | string) => { const bigIntValue = new BigNumber(value); const divisor = new BigNumber(10).pow(FixedU128Decimals); @@ -154,3 +119,7 @@ export const prettyNumbers = (number: number, lang?: string, opts?: Intl.NumberF export const roundNumber = (value: number | string = 0, round = 6) => { return +Number(value).toFixed(round); }; + +export function roundDownToSignificantDecimals(big: BigNumber, decimals: number) { + return big.prec(Math.max(0, big.e + 1) + decimals, 0); +} diff --git a/src/helpers/substrate.ts b/src/helpers/substrate.ts deleted file mode 100644 index 17a87f07..00000000 --- a/src/helpers/substrate.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { EventRecord } from '@polkadot/types/interfaces'; -import { ApiPromise } from '@polkadot/api'; - -export function containsError(events: EventRecord[], api: ApiPromise): boolean { - const errorEvents = events - // find/filter for failed events - .filter(({ event }) => api.events.system.ExtrinsicFailed.is(event)); - - return errorEvents.length > 0; -} - -// Adapted from https://polkadot.js.org/docs/api/cookbook/tx#how-do-i-get-the-decoded-enum-for-an-extrinsicfailed-event -export function getErrors(events: EventRecord[], api: ApiPromise) { - return ( - events - // find/filter for failed events - .filter(({ event }) => api.events.system.ExtrinsicFailed.is(event)) - // we know that data for system.ExtrinsicFailed is - // (DispatchError, DispatchInfo) - .map( - ({ - event: { - data: [error], - }, - }) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if ((error as any).isModule) { - // for module errors, we have the section indexed, lookup - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const decoded = api.registry.findMetaError((error as any).asModule); - const { docs, method, section } = decoded; - - return `${section}.${method}: ${docs.join(' ')}`; - } else { - // Other, CannotLookup, BadOrigin, no extra info - return error.toString(); - } - }, - ) - ); -} - -export function getEventBySectionAndMethod(events: EventRecord[], section: string, method: string) { - return events - .filter( - ({ event: { method: eventMethod, section: eventSection } }) => - eventSection.toLowerCase() === section.toLowerCase() && eventMethod.toLowerCase() === method.toLowerCase(), - ) - .map((event) => event.event); -} - -export function getEventMessages(events: EventRecord[]) { - return events.map(({ event: { data, method, section } }) => { - return `${section}.${method}( ${data.toString()} )`; - }); -} diff --git a/src/hooks/nabla/useContractRead.ts b/src/hooks/nabla/useContractRead.ts index 10589d85..66125d40 100644 --- a/src/hooks/nabla/useContractRead.ts +++ b/src/hooks/nabla/useContractRead.ts @@ -12,9 +12,9 @@ import { ApiPromise } from '../../services/polkadot/polkadotApi'; const isDevelopment = config.isDev; const ALICE = '6mfqoTMHrMeVMyKwjqomUjVomPMJ4AjdCm1VReFtk7Be8wqr'; -export type MessageCallErrorResult = ReadMessageResult & { type: 'error' | 'panic' | 'reverted' }; +type MessageCallErrorResult = ReadMessageResult & { type: 'error' | 'panic' | 'reverted' }; -export type UseContractReadProps = { +type UseContractReadProps = { abi: Dict; address: string | undefined; method: string; @@ -25,7 +25,7 @@ export type UseContractReadProps = { queryOptions: QueryOptions; }; -export type UseContractReadResult = UseQueryResult; +type UseContractReadResult = UseQueryResult; export function useContractRead( key: QueryKey, diff --git a/src/hooks/nabla/useTokenAmountOut.ts b/src/hooks/nabla/useTokenAmountOut.ts index 0f807a54..e36c9e6e 100644 --- a/src/hooks/nabla/useTokenAmountOut.ts +++ b/src/hooks/nabla/useTokenAmountOut.ts @@ -1,4 +1,5 @@ import BigNumber from 'big.js'; +import { UseQueryResult } from '@tanstack/react-query'; import { activeOptions, cacheKeys } from '../../constants/cache'; import { routerAbi } from '../../contracts/Router'; import { @@ -10,24 +11,24 @@ import { } from '../../helpers/contracts'; import { NABLA_ROUTER } from '../../constants/constants'; import { useContractRead } from './useContractRead'; -import { UseQueryResult } from '@tanstack/react-query'; import { useDebouncedValue } from '../useDebouncedValue'; -import { TOKEN_CONFIG, TokenType } from '../../constants/tokenConfig'; import { ApiPromise } from '../../services/polkadot/polkadotApi'; -import { FieldValues, UseFormReturn } from 'react-hook-form'; +import { UseFormReturn } from 'react-hook-form'; import { useEffect } from 'preact/hooks'; import Big from 'big.js'; +import { INPUT_TOKEN_CONFIG, InputTokenType, OUTPUT_TOKEN_CONFIG, OutputTokenType } from '../../constants/tokenConfig'; +import { SwapFormValues } from '../../components/Nabla/schema'; -export type UseTokenOutAmountProps = { +type UseTokenOutAmountProps = { wantsSwap: boolean; api: ApiPromise | null; fromAmountString: string; - fromToken: string; - toToken: string; + inputTokenType: InputTokenType; + outputTokenType: OutputTokenType; maximumFromAmount: BigNumber | undefined; xcmFees: string; slippageBasisPoints: number; - form: UseFormReturn; + form: UseFormReturn; }; export interface UseTokenOutAmountResult { @@ -37,23 +38,23 @@ export interface UseTokenOutAmountResult { refetch?: UseQueryResult['refetch']; } -export interface TokenOutData { +interface TokenOutData { amountOut: ContractBalance; swapFee: ContractBalance; effectiveExchangeRate: string; } -export function useTokenOutAmount({ +export function useTokenOutAmount({ wantsSwap, api, fromAmountString, - fromToken, - toToken, + inputTokenType, + outputTokenType, maximumFromAmount, xcmFees, slippageBasisPoints, form, -}: UseTokenOutAmountProps) { +}: UseTokenOutAmountProps) { const { setError, clearErrors } = form; const debouncedFromAmountString = useDebouncedValue(fromAmountString, 800); @@ -64,17 +65,17 @@ export function useTokenOutAmount({ // no action required } - const fromTokenDetails = TOKEN_CONFIG[fromToken as TokenType]; - const toTokenDetails = TOKEN_CONFIG[toToken as TokenType]; + const inputToken = INPUT_TOKEN_CONFIG[inputTokenType]; + const outputToken = OUTPUT_TOKEN_CONFIG[outputTokenType]; - const fromTokenDecimals = fromTokenDetails?.decimals; + const fromTokenDecimals = inputToken?.decimals; const amountInOriginal = fromTokenDecimals !== undefined && debouncedAmountBigDecimal !== undefined ? multiplyByPowerOfTen(debouncedAmountBigDecimal, fromTokenDecimals).toFixed(0, 0) : undefined; - const rawXcmFees = multiplyByPowerOfTen(BigNumber(xcmFees), fromTokenDetails.decimals).toFixed(0, 0); + const rawXcmFees = multiplyByPowerOfTen(BigNumber(xcmFees), inputToken?.decimals).toFixed(0, 0); const amountIn = amountInOriginal !== undefined ? clampedDifference(BigInt(amountInOriginal), BigInt(rawXcmFees)).toString() @@ -83,36 +84,41 @@ export function useTokenOutAmount({ const enabled = api !== undefined && wantsSwap && - fromTokenDetails !== undefined && - toTokenDetails !== undefined && + inputToken !== undefined && + outputToken !== undefined && debouncedAmountBigDecimal !== undefined && debouncedAmountBigDecimal.gt(new BigNumber(0)) && (maximumFromAmount === undefined || debouncedAmountBigDecimal.lte(maximumFromAmount)); const { isLoading, fetchStatus, data, error, refetch } = useContractRead( - [cacheKeys.tokenOutAmount, fromTokenDetails?.erc20Address, toTokenDetails?.erc20Address, amountIn], + [ + cacheKeys.tokenOutAmount, + inputToken.axelarEquivalent.pendulumErc20WrapperAddress, + outputToken.erc20WrapperAddress, + amountIn, + ], api, undefined, // Does not matter since noWalletAddressRequired is true { abi: routerAbi, address: NABLA_ROUTER, method: 'getAmountOut', - args: [amountIn, [fromTokenDetails?.erc20Address, toTokenDetails?.erc20Address]], + args: [amountIn, [inputToken.axelarEquivalent.pendulumErc20WrapperAddress, outputToken.erc20WrapperAddress]], noWalletAddressRequired: true, queryOptions: { ...activeOptions['30s'], enabled, }, parseSuccessOutput: (data) => { - if (toTokenDetails === undefined || fromTokenDetails === undefined || debouncedAmountBigDecimal === undefined) { + if (outputToken === undefined || inputToken === undefined || debouncedAmountBigDecimal === undefined) { return null; } const bigIntResponse = data[0]?.toBigInt(); const reducedResponse = (bigIntResponse * BigInt(10000 - slippageBasisPoints)) / 10000n; - const amountOut = parseContractBalanceResponse(toTokenDetails.decimals, reducedResponse); - const swapFee = parseContractBalanceResponse(toTokenDetails.decimals, data[1]); + const amountOut = parseContractBalanceResponse(outputToken.decimals, reducedResponse); + const swapFee = parseContractBalanceResponse(outputToken.decimals, data[1]); return { amountOut, @@ -128,10 +134,12 @@ export function useTokenOutAmount({ case 'error': return 'Something went wrong'; case 'panic': - return error.errorCode === 0x11 ? 'The input amount is too large' : 'Something went wrong'; + return error.errorCode === 0x11 + ? 'Insufficient liquidity for this exchange. Please try a smaller amount or try again later.' + : 'Something went wrong'; case 'reverted': return error.description === 'SwapPool: EXCEEDS_MAX_COVERAGE_RATIO' - ? 'The input amount is too large' + ? 'Insufficient liquidity for this exchange. Please try a smaller amount or try again later.' : 'Something went wrong'; default: return 'Something went wrong'; @@ -150,5 +158,8 @@ export function useTokenOutAmount({ } }, [error, pending, clearErrors, setError]); - return { isLoading: pending, enabled, data, refetch }; + const isInputStable = debouncedFromAmountString === fromAmountString; + const actualAmountInRaw = isInputStable && amountIn !== undefined ? amountIn : undefined; + + return { isLoading: pending, enabled, data, refetch, error, actualAmountInRaw }; } diff --git a/src/hooks/useBoolean.ts b/src/hooks/useBoolean.ts deleted file mode 100644 index e92e7a6d..00000000 --- a/src/hooks/useBoolean.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useCallback, useMemo, useState } from 'preact/compat'; - -export type UseBooleanActions = { - setValue: (val: boolean) => void; - toggle: () => void; - setTrue: () => void; - setFalse: () => void; -}; -export type UseBoolean = [boolean, UseBooleanActions]; - -export function useBoolean(initial = false): UseBoolean { - const [value, setValue] = useState(initial); - const toggle = useCallback(() => setValue((v) => !v), []); - const setTrue = useCallback(() => setValue(true), []); - const setFalse = useCallback(() => setValue(false), []); - const actions = useMemo(() => ({ setValue, toggle, setTrue, setFalse }), [setFalse, setTrue, toggle]); - return useMemo(() => [value, actions], [actions, value]); -} - -export default useBoolean; diff --git a/src/hooks/useClipboard.ts b/src/hooks/useClipboard.ts deleted file mode 100644 index 836af8c1..00000000 --- a/src/hooks/useClipboard.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useMemo } from 'preact/hooks'; - -export function useClipboard() { - return useMemo( - () => ({ - async copyToClipboard(value: string, notificationMessage?: string) { - try { - await navigator.clipboard.writeText(value); - // const message = notificationMessage || `Copied ${value} to clipboard`; - // toast(message, { type: 'info' }); - } catch (error) { - // toast(error, { type: 'error' }); - } - }, - }), - [], - ); -} diff --git a/src/hooks/useGetIcon.tsx b/src/hooks/useGetIcon.tsx new file mode 100644 index 00000000..5abb0066 --- /dev/null +++ b/src/hooks/useGetIcon.tsx @@ -0,0 +1,38 @@ +import { useChainId } from 'wagmi'; +import { polygon } from 'wagmi/chains'; + +import BRL from '../assets/coins/BRL.png'; +import EURC from '../assets/coins/EURC.png'; +import PEN from '../assets/coins/PEN.png'; +import USDT from '../assets/coins/USDT.png'; +import USDC from '../assets/coins/USDC.png'; +import USDC_POLYGON from '../assets/coins/USDC_POLYGON.svg'; + +import DefaultIcon from '../assets/coins/PEN.png'; +import { InputTokenType, OutputTokenType } from '../constants/tokenConfig'; + +type IconMap = Partial>; + +const icons: IconMap = { + brl: BRL, + eurc: EURC, + usdc: USDC, +}; + +const polygonIcons: IconMap = { + usdc: USDC_POLYGON, +}; + +const IconMaps: Record = { + [polygon.id]: polygonIcons, + default: icons, +}; + +export function useGetIcon(tokenType?: InputTokenType | OutputTokenType) { + const currentChainId = useChainId(); + const currentIconMap = IconMaps[currentChainId] ?? IconMaps.default; + + if (!tokenType) return DefaultIcon; + + return currentIconMap[tokenType] ?? IconMaps.default[tokenType] ?? DefaultIcon; +} diff --git a/src/hooks/useMainProcess.ts b/src/hooks/useMainProcess.ts new file mode 100644 index 00000000..43189d40 --- /dev/null +++ b/src/hooks/useMainProcess.ts @@ -0,0 +1,99 @@ +import { useState, useEffect, useCallback } from 'react'; + +// Configs, Types, constants +import { createStellarEphemeralSecret, sep24First } from '../services/anchor'; +import { ExecutionInput } from '../types'; +import { OUTPUT_TOKEN_CONFIG } from '../constants/tokenConfig'; + +import { fetchTomlValues, sep10, sep24Second } from '../services/anchor'; +// Utils +import { stringifyBigWithSignificantDecimals } from '../helpers/contracts'; +import { useConfig } from 'wagmi'; +import { OfframpingPhase, advanceOfframpingState, constructInitialState } from '../services/offrampingFlow'; +import { EventStatus, GenericEvent } from '../components/GenericEvent'; +import Big from 'big.js'; + +export const useMainProcess = () => { + // EXAMPLE mocking states + + // Approval already performed (scenario: service shut down after sending approval but before getting it's confirmation) + // let recoveryStatus = { + // approvalHash: '0xe2798e5c30915033e3d5aaecf2cb2704c31f0a68624013849729ac5c69f83048', + // swapHash: undefined, + // transactionRequest: {"routeType":"CALL_BRIDGE_CALL","target":"0xce16F69375520ab01377ce7B88f5BA8C48F8D666","data":"0x00","value":"511469868416439548","gasLimit":"556000","lastBaseFeePerGas":"3560652","maxFeePerGas":"1507121304","maxPriorityFeePerGas":"1500000000","gasPrice":"30003560652","requestId":"de321b5ab3f9989d67dab414b3556ece"} + // } + + // storageService.set(storageKeys.SQUIDROUTER_RECOVERY_STATE, recoveryStatus ); + // storageService.set(storageKeys.OFFRAMP_STATUS, OperationStatus.Sep6Completed); + + const [offrampingStarted, setOfframpingStarted] = useState(false); + const [offrampingPhase, setOfframpingPhase] = useState(undefined); + const [sep24Url, setSep24Url] = useState(undefined); + const wagmiConfig = useConfig(); + + const [events, setEvents] = useState([]); + + const addEvent = (message: string, status: EventStatus) => { + console.log('Add event', message, status); + setEvents((prevEvents) => [...prevEvents, { value: message, status }]); + }; + + // Main submit handler. Offramp button. + const handleOnSubmit = useCallback( + ({ inputTokenType, outputTokenType, amountInUnits, nablaAmountInRaw, minAmountOutUnits }: ExecutionInput) => { + if (offrampingStarted || offrampingPhase !== undefined) return; + + (async () => { + const stellarEphemeralSecret = createStellarEphemeralSecret(); + + const outputToken = OUTPUT_TOKEN_CONFIG[outputTokenType]; + const tomlValues = await fetchTomlValues(outputToken.tomlFileUrl!); + + const truncatedAmountToOfframp = stringifyBigWithSignificantDecimals(Big(minAmountOutUnits), 2); + + const sep10Token = await sep10(tomlValues, stellarEphemeralSecret, addEvent); + + setOfframpingStarted(true); + + const anchorSessionParams = { + token: sep10Token, + tomlValues: tomlValues, + tokenConfig: outputToken, + offrampAmount: truncatedAmountToOfframp, + }; + const firstSep24Response = await sep24First(anchorSessionParams); + console.log('sep24 url:', firstSep24Response.url); + setSep24Url(firstSep24Response.url); + + const secondSep24Response = await sep24Second(firstSep24Response, anchorSessionParams!); + + console.log('secondSep24Response', secondSep24Response); + + const initialState = await constructInitialState({ + inputTokenType, + outputTokenType, + amountIn: amountInUnits, + nablaAmountInRaw, + amountOut: minAmountOutUnits, + sepResult: secondSep24Response, + }); + + setOfframpingPhase(initialState?.phase); + })(); + }, + [], + ); + + useEffect(() => { + (async () => { + const nextState = await advanceOfframpingState({ renderEvent: addEvent, wagmiConfig }); + setOfframpingPhase(nextState?.phase); + })(); + }, [offrampingPhase]); + + return { + handleOnSubmit, + sep24Url, + offrampingPhase, + }; +}; diff --git a/src/layouts/index.tsx b/src/layouts/index.tsx new file mode 100644 index 00000000..188e5e95 --- /dev/null +++ b/src/layouts/index.tsx @@ -0,0 +1,15 @@ +import { FC } from 'preact/compat'; +import { Navbar } from '../components/Navbar'; + +interface BaseLayoutProps { + main: ReactNode; + modals?: ReactNode; +} + +export const BaseLayout: FC = ({ main, modals }) => ( + <> + {modals} + + {main} + +); diff --git a/src/pages/landing/index.tsx b/src/pages/landing/index.tsx deleted file mode 100644 index 20581be2..00000000 --- a/src/pages/landing/index.tsx +++ /dev/null @@ -1,308 +0,0 @@ -import Big from 'big.js'; -import { useState, useEffect, useRef } from 'react'; - -import '../../../App.css'; -import InputBox from '../../components/InputKeys'; -import { SwapOptions } from '../../components/InputKeys'; -import EventBox from '../../components/GenericEvent'; -import { GenericEvent, EventStatus } from '../../components/GenericEvent'; -import { fetchTomlValues, IAnchorSessionParams, Sep24Result, getEphemeralKeys, sep10 } from '../../services/anchor'; -import { - setUpAccountAndOperations, - StellarOperations, - submitOfframpTransaction, - cleanupStellarEphemeral, -} from '../../services/stellar'; -import { executeSpacewalkRedeem } from '../../services/polkadot'; -import Sep24 from '../../components/Sep24Component'; -import { useCallback } from 'preact/compat'; -import { useGlobalState } from '../../GlobalStateProvider'; -import { fetchSigningServicePK } from '../../services/signingService'; -import { TOKEN_CONFIG, TokenDetails } from '../../constants/tokenConfig'; -import { performSwap } from '../../services/nabla'; -import { TRANSFER_WAITING_TIME_SECONDS } from '../../constants/constants'; -import { - waitForTokenReceptionEvent, - getEphemeralAccount, - checkBalance, - fundEphemeralAccount, - cleanEphemeralAccount, -} from '../../services/polkadot/ephemeral'; -import { stringifyBigWithSignificantDecimals } from '../../helpers/contracts'; -import { useSquidRouterSwap, TransactionStatus } from '../../services/squidrouter'; -import { decimalToCustom } from '../../helpers/parseNumbers'; -import { TokenType } from '../../constants/tokenConfig'; - -enum OperationStatus { - Idle, - Submitting, - SettingUpStellar, - Redeeming, - FinalizingOfframp, - Completed, - Error, -} - -export interface ExecutionInput { - assetToOfframp: TokenType; - amountIn: Big; - swapOptions: SwapOptions | undefined; // undefined means direct offramp -} - -function Landing() { - // system status - const [status, setStatus] = useState(OperationStatus.Idle); - const [executionInput, setExecutionInput] = useState(undefined); - - // events state and refs - const [events, setEvents] = useState([]); - const eventsEndRef = useRef(null); - const [activeEventIndex, setActiveEventIndex] = useState(-1); - - // seession and operations states - const [fundingPK, setFundingPK] = useState(null); - const [anchorSessionParams, setAnchorSessionParams] = useState(null); - const [stellarOperations, setStellarOperations] = useState(null); - const [sep24Result, setSep24Result] = useState(null); - - // UI states - const [showSep24, setShowSep24] = useState(false); - const [canInitiate, setCanInitiate] = useState(false); - const [backendError, setBackendError] = useState(false); - - //Squidrouter hook - const [amountInNative, setAmountIn] = useState('0'); - const { transactionStatus, executeSquidRouterSwap, error } = useSquidRouterSwap(amountInNative); - const handleOnSubmit = async ({ assetToOfframp, amountIn, swapOptions }: ExecutionInput) => { - // we always want swap now, but for now we hardcode the starting token - setAmountIn(decimalToCustom(amountIn, TOKEN_CONFIG.usdc.decimals).toFixed()); - setExecutionInput({ assetToOfframp, amountIn, swapOptions }); - - - const tokenConfig: TokenDetails = TOKEN_CONFIG[assetToOfframp]; - const values = await fetchTomlValues(tokenConfig.tomlFileUrl!); - - const amountToOfframp = swapOptions !== undefined ? swapOptions.minAmountOut : amountIn; - console.log(amountToOfframp); - - const truncatedAmountToOfframp = stringifyBigWithSignificantDecimals(amountToOfframp.round(2, 0), 2); - - const token = await sep10(values, addEvent); - - setAnchorSessionParams({ - token, - tomlValues: values, - tokenConfig, - offrampAmount: truncatedAmountToOfframp, - }); - // showing (rendering) the Sep24 component will trigger the Sep24 process - setShowSep24(true); - setStatus(OperationStatus.Submitting); - }; - - const handleOnSep24Completed = async (result: Sep24Result) => { - setShowSep24(false); - - // log the result - addEvent( - `SEP24 completed, amount: ${result.amount}, memo: ${result.memo}, offramping account: ${result.offrampingAccount}`, - EventStatus.Waiting, - ); - setSep24Result(result); - - if (executionInput === undefined) return; - const { assetToOfframp, amountIn, swapOptions } = executionInput; - // Start the squid router process - executeSquidRouterSwap(); - - // log ephemeral pk - const ephemeralAccount = getEphemeralAccount().address; - addEvent(`Pendulum ephemeral account: ${ephemeralAccount}`, EventStatus.Waiting); - - // Wait for ephemeral to receive native balance - // And wait for ephemeral to receive the funds of the token to be offramped - - const tokenToReceive = swapOptions ? TOKEN_CONFIG.usdc.currencyId : TOKEN_CONFIG[assetToOfframp].currencyId; - - console.log('Waiting to receive token: ', tokenToReceive); - const tokenTransferEvent = await waitForTokenReceptionEvent(tokenToReceive, TRANSFER_WAITING_TIME_SECONDS * 1000); - console.log('token received', tokenTransferEvent); - - // call checkBalance until it returns true - let ready; - do { - ready = await checkBalance(); - } while (!ready); - - if (swapOptions) { - const enteredAmountDecimal = new Big(result.amount); - //TESTING commented since we are mocking the response of the KYC - // if (enteredAmountDecimal.gt(swapOptions.minAmountOut)) { - // addEvent( - // `The amount you entered is too high. Maximum possible amount to offramp: ${swapOptions.minAmountOut.toString()}), you entered: ${ - // result.amount - // }.`, - // EventStatus.Error, - // ); - // return; - // } - - await performSwap( - { - amountInRaw: tokenTransferEvent.amountRaw, - assetOut: assetToOfframp, - assetIn: swapOptions.assetIn, - minAmountOut: swapOptions.minAmountOut, - }, - addEvent, - ); - } - - // set up the ephemeral account and operations we will later neeed - try { - addEvent('Settings stellar accounts', EventStatus.Waiting); - const operations = await setUpAccountAndOperations( - fundingPK!, - result, - getEphemeralKeys(), - anchorSessionParams!.tokenConfig, - addEvent, - ); - setStellarOperations(operations); - } catch (error) { - addEvent(`Stellar setup failed ${error}`, EventStatus.Error); - return; - } - - addEvent('Stellar things done!', EventStatus.Waiting); - - //this will trigger redeem - setStatus(OperationStatus.Redeeming); - }; - - const executeRedeem = useCallback( - async (sepResult: Sep24Result) => { - try { - const ephemeralAccount = getEphemeralAccount(); - await executeSpacewalkRedeem( - getEphemeralKeys().publicKey(), - sepResult.amount, - ephemeralAccount, - anchorSessionParams!.tokenConfig, - addEvent, - ); - } catch (error) { - console.log(error); - return; - } - - addEvent('Redeem process completed, executing offramp transaction', EventStatus.Waiting); - - //this will trigger finalizeOfframp - setStatus(OperationStatus.FinalizingOfframp); - }, - [anchorSessionParams], - ); - - const finalizeOfframp = useCallback(async () => { - try { - await submitOfframpTransaction(stellarOperations!.offrampingTransaction, addEvent); - } catch (error) { - console.error('Offramp failed', error); - addEvent('Offramp transaction failed', EventStatus.Error); - return; - } - - addEvent('Offramp Submitted! Funds should be available shortly', EventStatus.Success); - - // we may not necessarily need to show the user an error, since the offramp transaction is already submitted - // and successful - // This will not affect the user - await cleanupStellarEphemeral(stellarOperations!.mergeAccountTransaction, addEvent); - await cleanEphemeralAccount(executionInput?.assetToOfframp!); - }, [stellarOperations]); - - const addEvent = (message: string, status: EventStatus) => { - setEvents((prevEvents) => [...prevEvents, { value: message, status }]); - setActiveEventIndex((prevIndex) => prevIndex + 1); - }; - - const scrollToLatestEvent = () => { - if (eventsEndRef.current) { - eventsEndRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' }); - } - }; - - // Fund the ephemeral account after the squid swap is completed - - // Should we fund this after approval or after the swap is completed? - // Right now, the SwapCompleted variant is never set. - useEffect(() => { - console.log('Transaction status: ', transactionStatus); - if (transactionStatus == TransactionStatus.SpendingApproved) { - console.log('Funding account after squid swap is completed'); - addEvent('Approval to Squidrouter completed', EventStatus.Success); - fundEphemeralAccount(); - } - }, [transactionStatus, error]); - - useEffect(() => { - scrollToLatestEvent(); - }, [events]); - - useEffect(() => { - const initiate = async () => { - try { - const fundingPK = await fetchSigningServicePK(); - setFundingPK(fundingPK); - setCanInitiate(true); - } catch (error) { - setBackendError(true); - console.error('Error fetching token', error); - } - }; - - initiate().catch(console.error); - }, []); - - useEffect(() => { - switch (status) { - case OperationStatus.Redeeming: - executeRedeem(sep24Result!).catch(console.error); - return; - case OperationStatus.FinalizingOfframp: - finalizeOfframp().catch(console.error); - return; - } - }, [status, executeRedeem, finalizeOfframp, sep24Result]); - - return ( -
- {backendError && ( -
-

Service is Down

-
Please try again later or reload the page.
-
- )} - {canInitiate && } - {showSep24 && ( -
- -
- )} -
- {events.map((event, index) => ( - - ))} -
-
-
- ); -} - -export default Landing; diff --git a/src/pages/progress/index.tsx b/src/pages/progress/index.tsx new file mode 100644 index 00000000..b3774f39 --- /dev/null +++ b/src/pages/progress/index.tsx @@ -0,0 +1,34 @@ +import { useEffect } from 'preact/hooks'; +import { ExclamationCircleIcon } from '@heroicons/react/20/solid'; +import { Box } from '../../components/Box'; +import { BaseLayout } from '../../layouts'; + +const handleTabClose = (event: Event) => { + event.preventDefault(); +}; + +export const ProgressPage = () => { + useEffect(() => { + window.addEventListener('beforeunload', handleTabClose); + + return () => { + window.removeEventListener('beforeunload', handleTabClose); + }; + }, []); + + const main = ( +
+ +
+ +

DO NOT CLOSE THIS TAB!

+

Your transaction is in progress.

+ +

Closing this tab can result in the transaction not being processed.

+
+
+
+ ); + + return ; +}; diff --git a/src/pages/swap/index.tsx b/src/pages/swap/index.tsx new file mode 100644 index 00000000..cb15daa1 --- /dev/null +++ b/src/pages/swap/index.tsx @@ -0,0 +1,298 @@ +import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; +import { useAccount } from 'wagmi'; +import Big from 'big.js'; +import { ArrowDownIcon } from '@heroicons/react/20/solid'; + +import { LabeledInput } from '../../components/LabeledInput'; +import { BenefitsList } from '../../components/BenefitsList'; +import { FeeCollapse } from '../../components/FeeCollapse'; +import { useSwapForm } from '../../components/Nabla/useSwapForm'; +import { ApiPromise, getApiManagerInstance } from '../../services/polkadot/polkadotApi'; +import { useTokenOutAmount } from '../../hooks/nabla/useTokenAmountOut'; +import { PoolSelectorModal } from '../../components/InputKeys/SelectionModal'; +import { ExchangeRate } from '../../components/ExchangeRate'; +import { AssetNumericInput } from '../../components/AssetNumericInput'; +import { SwapSubmitButton } from '../../components/buttons/SwapSubmitButton'; +import { BankDetails } from './sections/BankDetails'; +import { config } from '../../config'; +import { INPUT_TOKEN_CONFIG, InputTokenType, OUTPUT_TOKEN_CONFIG, OutputTokenType } from '../../constants/tokenConfig'; +import { BaseLayout } from '../../layouts'; + +import { useMainProcess } from '../../hooks/useMainProcess'; +import { multiplyByPowerOfTen, stringifyBigWithSignificantDecimals } from '../../helpers/contracts'; +import { ProgressPage } from '../progress'; + +const Arrow = () => ( +
+ +
+); + +export const SwapPage = () => { + const [isSubmitButtonDisabled, setIsSubmitButtonDisabled] = useState(true); + const [isExchangeSectionSubmitted, setIsExchangeSectionSubmitted] = useState(false); + const [isExchangeSectionSubmittedError, setIsExchangeSectionSubmittedError] = useState(false); + const [isQuoteSubmitted, setIsQuoteSubmitted] = useState(false); + const formRef = useRef(null); + + const [api, setApi] = useState(null); + + const { isDisconnected } = useAccount(); + + useEffect(() => { + const initializeApiManager = async () => { + const manager = await getApiManagerInstance(); + const { api } = await manager.getApiComponents(); + setApi(api); + }; + + initializeApiManager().catch(console.error); + }, []); + + // Main process hook + const { handleOnSubmit, sep24Url, offrampingPhase } = useMainProcess(); + + const { + tokensModal: [modalType, setModalType], + onFromChange, + onToChange, + form, + fromAmount, + fromAmountString, + from, + to, + reset, + } = useSwapForm(); + + const fromToken = from ? INPUT_TOKEN_CONFIG[from] : undefined; + const toToken = to ? OUTPUT_TOKEN_CONFIG[to] : undefined; + + useEffect(() => { + if (form.formState.isDirty && isExchangeSectionSubmitted && isDisconnected) { + setIsExchangeSectionSubmitted(false); + reset(); + } + }, [form.formState.isDirty, isDisconnected, isExchangeSectionSubmitted, reset]); + + const tokenOutData = useTokenOutAmount({ + wantsSwap: true, + api, + inputTokenType: from, + outputTokenType: to, + maximumFromAmount: undefined, + slippageBasisPoints: config.swap.slippageBasisPoints, + fromAmountString, + xcmFees: config.xcm.fees, + form, + }); + + const inputAmountIsStable = + tokenOutData.actualAmountInRaw !== undefined && BigInt(tokenOutData.actualAmountInRaw) > 0n; + + // Check only the first part of the form (without Bank Details) + const isFormValidWithoutBankDetails = useMemo(() => { + const errors = form.formState.errors; + const noErrors = !errors.from && !errors.to && !errors.fromAmount && !errors.toAmount; + const isValid = + Boolean(from) && Boolean(to) && Boolean(fromAmount) && Boolean(tokenOutData.data?.amountOut.preciseString); + + return noErrors && isValid; + }, [form.formState.errors, from, fromAmount, to, tokenOutData.data?.amountOut.preciseString]); + + function onSubmit(e: Event) { + e.preventDefault(); + + if (isSubmitButtonDisabled || !inputAmountIsStable || tokenOutData.actualAmountInRaw === undefined) return; + + if (!isExchangeSectionSubmitted) { + if (isFormValidWithoutBankDetails) { + setIsExchangeSectionSubmittedError(false); + setIsExchangeSectionSubmitted(true); + } else { + setIsExchangeSectionSubmittedError(true); + } + + return; + } + + if (fromAmount === undefined) { + console.log('Input amount is undefined'); + return; + } + + const minimumOutputAmount = tokenOutData.data?.amountOut; + if (minimumOutputAmount === undefined) { + console.log('Output amount is undefined'); + return; + } + + console.log('starting ....'); + + handleOnSubmit({ + inputTokenType: from as InputTokenType, + outputTokenType: to as OutputTokenType, + amountInUnits: fromAmountString, + nablaAmountInRaw: tokenOutData.actualAmountInRaw, + minAmountOutUnits: minimumOutputAmount.preciseString, + }); + } + + useEffect(() => { + if (isExchangeSectionSubmitted) { + formRef.current && formRef.current.scrollIntoView({ behavior: 'smooth', block: 'end' }); + } + }, [isExchangeSectionSubmitted]); + + useEffect(() => { + if (tokenOutData.data) { + const toAmount = tokenOutData.data.amountOut.preciseBigDecimal.round(2, 0); + form.setValue('toAmount', stringifyBigWithSignificantDecimals(toAmount, 2)); + + setIsQuoteSubmitted(false); + } else { + form.setValue('toAmount', ''); + } + }, [form, tokenOutData.data]); + + // Check if the Submit button should be enabled + useEffect(() => { + // Validate only the first part of the form (without Bank Details) + if (!isExchangeSectionSubmitted && isFormValidWithoutBankDetails) { + setIsSubmitButtonDisabled(false); + } + // Validate the whole form (with Bank Details) + else if (isExchangeSectionSubmitted /*&& form.formState.isValid*/) { + setIsSubmitButtonDisabled(false); + } else { + setIsSubmitButtonDisabled(true); + } + }, [form.formState, form.formState.isValid, isExchangeSectionSubmitted, isFormValidWithoutBankDetails]); + + const ReceiveNumericInput = useMemo( + () => ( + setModalType('to')} + registerInput={form.register('toAmount')} + disabled={isQuoteSubmitted || tokenOutData.isLoading} + readOnly={true} + /> + ), + [toToken, form, isQuoteSubmitted, tokenOutData.isLoading, setModalType], + ); + + const WidthrawNumericInput = useMemo( + () => ( + setIsQuoteSubmitted(true) })} + tokenType={from} + tokenSymbol={fromToken?.assetSymbol} + onClick={() => setModalType('from')} + /> + ), + [form, fromToken, setModalType], + ); + + function getCurrentErrorMessage() { + if (isExchangeSectionSubmittedError) { + return 'You must first enter the amount you wish to withdraw.'; + } + + const amountOut = tokenOutData.data?.amountOut; + + if (amountOut !== undefined && toToken !== undefined) { + const maxAmountRaw = Big(toToken.maxWithdrawalAmountRaw); + const minAmountRaw = Big(toToken.minWithdrawalAmountRaw); + + if (maxAmountRaw.lt(Big(amountOut.rawBalance))) { + const maxAmountUnits = multiplyByPowerOfTen(maxAmountRaw, -toToken.decimals); + return `Maximum withdrawal amount is ${stringifyBigWithSignificantDecimals(maxAmountUnits, 2)} ${ + toToken.stellarAsset.code.string + }.`; + } + + if (config.test.overwriteMinimumTransferAmount === false && minAmountRaw.gt(Big(amountOut.rawBalance))) { + const minAmountUnits = multiplyByPowerOfTen(minAmountRaw, -toToken.decimals); + return `Minimum withdrawal amount is ${stringifyBigWithSignificantDecimals(minAmountUnits, 2)} ${ + toToken.stellarAsset.code.string + }.`; + } + } + + return tokenOutData.error; + } + + const definitions = + modalType === 'from' + ? Object.entries(INPUT_TOKEN_CONFIG).map(([key, value]) => ({ + type: key as InputTokenType, + assetSymbol: value.assetSymbol, + })) + : Object.entries(OUTPUT_TOKEN_CONFIG).map(([key, value]) => ({ + type: key as OutputTokenType, + assetSymbol: value.stellarAsset.code.string, + })); + + const modals = ( + setModalType(undefined)} + isLoading={false} + /> + ); + + console.log('IssubmitButtonDisabled: ', isSubmitButtonDisabled); + + if (offrampingPhase !== undefined) { + return ; + } + + const main = ( +
+
+

Withdraw

+ + + +

{getCurrentErrorMessage()}

+ + +
+ +
+ {isExchangeSectionSubmitted ? ( + + ) : ( + <> + )} + {sep24Url !== undefined ? ( + + Start Offramping + + ) : ( + + )} + +
+ ); + + return ; +}; diff --git a/src/pages/swap/sections/BankDetails.tsx b/src/pages/swap/sections/BankDetails.tsx new file mode 100644 index 00000000..3bf5b7de --- /dev/null +++ b/src/pages/swap/sections/BankDetails.tsx @@ -0,0 +1,26 @@ +import { UseFormRegisterReturn } from 'react-hook-form'; +import { FC } from 'preact/compat'; +import { LabeledInput } from '../../../components/LabeledInput'; +import { SwapFormValues } from '../../../components/Nabla/schema'; +import { TextInput } from '../../../components/TextInput'; + +interface BankDetailsProps { + registerBankAccount: UseFormRegisterReturn; + registerTaxNumber: UseFormRegisterReturn; +} + +export const BankDetails: FC = ({ registerBankAccount, registerTaxNumber }) => ( +
+
+

Bank transfer details

+

Which bank account should we send the funds to?

+ + } + /> +
+ } /> +
+); diff --git a/src/queries/constants.ts b/src/queries/constants.ts deleted file mode 100644 index 924c2b3f..00000000 --- a/src/queries/constants.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const cacheKeys = { - balance: 'balance', - accountBalance: 'accountBalance', - tokenAllowance: 'tokenAllowance', -}; - -export const MINUTE_IN_MILLISECONDS = 60 * 1000; -export const SECONDS_IN_A_DAY = 86400; -export const BLOCK_TIME_SEC = 12; diff --git a/src/queries/helpers.ts b/src/queries/helpers.ts deleted file mode 100644 index 3a5d3e9f..00000000 --- a/src/queries/helpers.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ApiPromise } from '@polkadot/api'; -import { SubmittableResultValue } from '@polkadot/api-base/types'; -import type { QueryKey, UseQueryOptions } from '@tanstack/react-query'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type QueryOptions = Partial< - Omit, 'queryKey' | 'queryFn'> ->; - -export const emptyFn = () => undefined; -export const emptyCacheKey = ['']; - -// TODO: complete - improve error parsing -export const parseTransactionError = (result: SubmittableResultValue | undefined, api: ApiPromise) => { - if (!result?.dispatchError) return undefined; - if (result.dispatchError.isModule) { - // for module errors, we have the section indexed, lookup - const decoded = api.registry.findMetaError(result.dispatchError.asModule); - const { docs, name, section } = decoded; - console.log(`${section}.${name}: ${docs.join(' ')}`); - } else { - // Other, CannotLookup, BadOrigin, no extra info - console.log(result.dispatchError.toString()); - } -}; - -export const gasDefaults = { - refTime: '120000000000', - proofSize: '1200000', -}; diff --git a/src/services/anchor/index.ts b/src/services/anchor/index.ts index 7184e8ad..49ae6752 100644 --- a/src/services/anchor/index.ts +++ b/src/services/anchor/index.ts @@ -1,42 +1,42 @@ import { Transaction, Keypair, Networks } from 'stellar-sdk'; import { EventStatus } from '../../components/GenericEvent'; -import { TokenDetails } from '../../constants/tokenConfig'; -export interface TomlValues { +import { OutputTokenDetails } from '../../constants/tokenConfig'; +import { fetchSigningServiceAccountId } from '../signingService'; +import { config } from '../../config'; + +interface TomlValues { signingKey?: string; webAuthEndpoint?: string; sep24Url?: string; + sep6Url?: string; + kycServer?: string; } -export interface ISep24Intermediate { +interface ISep24Intermediate { url: string; id: string; } -export interface IAnchorSessionParams { +interface IAnchorSessionParams { token: string; tomlValues: TomlValues; - tokenConfig: TokenDetails; + tokenConfig: OutputTokenDetails; offrampAmount: string; } -export interface Sep24Result { +export interface SepResult { amount: string; memo: string; memoType: string; offrampingAccount: string; } +export function createStellarEphemeralSecret() { + const ephemeralKeys = Keypair.random(); + return ephemeralKeys.secret(); +} + const exists = (value?: string | null): value is string => !!value && value?.length > 0; -let ephemeralKeys: Keypair | null; - -export const getEphemeralKeys = () => { - if (ephemeralKeys) { - return ephemeralKeys; - } else { - ephemeralKeys = Keypair.random(); - return ephemeralKeys; - } -}; export const fetchTomlValues = async (TOML_FILE_URL: string): Promise => { const response = await fetch(TOML_FILE_URL); @@ -59,11 +59,14 @@ export const fetchTomlValues = async (TOML_FILE_URL: string): Promise void, ): Promise => { const { signingKey, webAuthEndpoint } = tomlValues; @@ -72,7 +75,7 @@ export const sep10 = async ( throw new Error('Missing values in TOML file'); } const NETWORK_PASSPHRASE = Networks.PUBLIC; - const ephemeralKeys = getEphemeralKeys(); + const ephemeralKeys = Keypair.fromSecret(stellarEphemeralSecret); const accountId = ephemeralKeys.publicKey(); const urlParams = new URLSearchParams({ account: accountId, @@ -120,16 +123,79 @@ export const sep10 = async ( return token; }; -export async function sep24First( - sessionParams: IAnchorSessionParams, - renderEvent: (event: string, status: EventStatus) => void, -): Promise { +// TODO modify according to the anchor's requirements and implementation +// we should be able to do the whole flow on this function since we have all the +// information we need +/* +export async function sep6First(sessionParams: IAnchorSessionParams): Promise { + const { token, tomlValues } = sessionParams; + const { sep6Url } = tomlValues; + + return { + amount: '10.4', + memo: 'a memo', + memoType: 'text', + offrampingAccount: 'GCUHGQ6LY3L2NAB7FX2LJGUJFCG6LKAQHVIMJLZNNBMCZUQNBPJTXE6O', + }; + + const sep6Params = new URLSearchParams({ + asset_code: sessionParams.tokenConfig.assetCode!, + type: 'bank_account', + dest: '3eE4729a-123B-45c6-8d7e-F9aD567b9c1e', // Ntokens crashes when sending destination, complains of not having it?? + }); + + const fetchUrl = `${sep6Url}/withdraw?`; + const sep6Response = await fetch(fetchUrl + sep6Params, { + method: 'GET', + headers: { 'Content-Type': 'application/x-www-form-urlencoded', Authorization: `Bearer ${token}` }, + }); + console.log(sep6Response); + if (sep6Response.status !== 200) { + console.log(await sep6Response.json(), sep6Response.toString()); + throw new Error(`Failed to initiate SEP-6: ${sep6Response.statusText}`); + } + + const { type, id } = await sep6Response.json(); + if (type !== 'interactive_customer_info_needed') { + throw new Error(`Unexpected SEP-6 type: ${type}`); + } + //return { transactionId: id }; +}*/ + +/* +export async function sep12First(sessionParams: IAnchorSessionParams): Promise { + const { token, tomlValues } = sessionParams; + const { sep6Url } = tomlValues; + + const sep12Params = new URLSearchParams({ + account: '3eE4729a-123B-45c6-8d7e-F9aD567b9c1e', + }); + + const fetchUrl = `${sep6Url}/customer`; + const sep12Response = await fetch(fetchUrl, { + method: 'PUT', + headers: { 'Content-Type': 'application/x-www-form-urlencoded', Authorization: `Bearer ${token}` }, + body: sep12Params.toString(), + }); + console.log(sep12Response); + if (sep12Response.status !== 200) { + console.log(await sep12Response.json(), sep12Response.toString()); + throw new Error(`Failed to initiate SEP-6: ${sep12Response.statusText}`); + } + //>???? +}*/ + +export async function sep24First(sessionParams: IAnchorSessionParams): Promise { + if (config.test.mockSep24) { + return { url: 'https://www.example.com', id: '1234' }; + } + const { token, tomlValues } = sessionParams; const { sep24Url } = tomlValues; // at this stage, assetCode should be defined, if the config is consistent. const sep24Params = new URLSearchParams({ - asset_code: sessionParams.tokenConfig.assetCode!, + asset_code: sessionParams.tokenConfig.stellarAsset.code.string, amount: sessionParams.offrampAmount, }); @@ -141,13 +207,11 @@ export async function sep24First( }); if (sep24Response.status !== 200) { console.log(await sep24Response.json(), sep24Params.toString()); - renderEvent(`Failed to initiate SEP-24: ${sep24Response.statusText}, ${fetchUrl}`, EventStatus.Error); throw new Error(`Failed to initiate SEP-24: ${sep24Response.statusText}`); } const { type, url, id } = await sep24Response.json(); if (type !== 'interactive_customer_info_needed') { - renderEvent(`Unexpected SEP-24 type: ${type}`, EventStatus.Error); throw new Error(`Unexpected SEP-24 type: ${type}`); } @@ -157,11 +221,20 @@ export async function sep24First( export async function sep24Second( sep24Values: ISep24Intermediate, sessionParams: IAnchorSessionParams, -): Promise { +): Promise { const { id } = sep24Values; const { token, tomlValues } = sessionParams; const { sep24Url } = tomlValues; + if (config.test.mockSep24) { + return { + amount: sessionParams.offrampAmount, + memo: 'MYK1722323689', + memoType: 'text', + offrampingAccount: await fetchSigningServiceAccountId(), + }; + } + let status; do { await new Promise((resolve) => setTimeout(resolve, 1000)); diff --git a/src/services/evmTransactions.ts b/src/services/evmTransactions.ts new file mode 100644 index 00000000..95c744d8 --- /dev/null +++ b/src/services/evmTransactions.ts @@ -0,0 +1,6 @@ +import { waitForTransactionReceipt } from '@wagmi/core'; +import { Config } from 'wagmi'; + +export async function waitForEvmTransaction(hash: `0x${string}`, wagmiConfig: Config) { + const result = await waitForTransactionReceipt(wagmiConfig, { hash }); +} diff --git a/src/services/localStorage.tsx b/src/services/localStorage.tsx deleted file mode 100644 index 0b46eaee..00000000 --- a/src/services/localStorage.tsx +++ /dev/null @@ -1,39 +0,0 @@ -const exists = (value?: string | null): value is string => !!value && value.length > 0; - -export interface Storage { - get: (key: string, defaultValue?: string) => string | undefined; - getParsed: (key: string, defaultValue?: T, parses?: (text: string) => T | undefined) => T | undefined; - getNumber: (key: string) => number | undefined; - getBoolean: (key: string) => boolean | undefined; - set: (key: string, value: unknown) => void; - remove: (key: string) => void; -} - -export const storageService: Storage = { - get: (key, defaultValue?) => { - if (!localStorage) return defaultValue; - const value = localStorage.getItem(key); - return exists(value) ? value : defaultValue; - }, - getParsed: (key, defaultValue?, parser = JSON.parse) => { - if (!localStorage) return defaultValue; - const value = localStorage.getItem(key); - if (!exists(value)) return defaultValue; - try { - return parser(value as string); - } catch (e) { - return defaultValue; - } - }, - - getNumber: (key: string) => Number(localStorage?.getItem(key)), - getBoolean: (key: string) => Boolean(localStorage?.getItem(key)), - - set: (key, value?) => - localStorage?.setItem( - key, - (value && typeof value === 'object') || Array.isArray(value) ? JSON.stringify(value) : String(value), - ), - - remove: (key) => localStorage?.removeItem(key), -}; diff --git a/src/services/metamask/index.ts b/src/services/metamask/index.ts deleted file mode 100644 index 1a514e11..00000000 --- a/src/services/metamask/index.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { enablePolkadotSnap } from '@chainsafe/metamask-polkadot-adapter'; -import type { MetamaskPolkadotSnap } from '@chainsafe/metamask-polkadot-adapter/build/snap'; -import type { InjectedMetamaskExtension } from '@chainsafe/metamask-polkadot-adapter/src/types'; -import type { SnapNetworks } from '@chainsafe/metamask-polkadot-types'; -import { Signer } from '@polkadot/api/types'; -import { web3EnablePromise } from '@polkadot/extension-dapp'; -import type { InjectedExtension } from '@polkadot/extension-inject/types'; -import { SignerPayloadJSON, SignerPayloadRaw, SignerResult } from '@polkadot/types/types'; -import { WalletAccount } from '@talismn/connect-wallets'; -import { TenantName } from '../../models/Tenant'; - -export const WALLET_SOURCE_METAMASK = 'metamask'; - -export function tenantToSnapNetwork(tenantName: TenantName): SnapNetworks { - switch (tenantName) { - case TenantName.Pendulum: - return 'polkadot'; - case TenantName.Amplitude: - return 'kusama'; - default: - return 'kusama'; - } -} - -export async function installPolkadotSnap(relayChain: SnapNetworks): Promise { - try { - await enablePolkadotSnap({ networkName: relayChain }); - console.info('Snap installed!!'); - return true; - } catch (err) { - console.error(err); - return false; - } -} - -export async function isPolkadotSnapInstalled(): Promise { - return !!(await getInjectedMetamaskExtension()); -} - -export async function getInjectedMetamaskExtension(): Promise { - const extensions = await web3EnablePromise; - return getMetamaskExtension(extensions || []) || null; -} - -function getMetamaskExtension(extensions: InjectedExtension[]): InjectedMetamaskExtension | undefined { - return extensions.find((item) => item.name === 'metamask-polkadot-snap') as unknown as - | InjectedMetamaskExtension - | undefined; -} - -export interface SnapInitializationResponse { - isSnapInstalled: boolean; - snap?: MetamaskPolkadotSnap; -} - -export async function initiatePolkadotSnap(relayChain: SnapNetworks): Promise { - try { - console.info('Attempting to connect to snap...'); - const metamaskPolkadotSnap = await enablePolkadotSnap({ networkName: relayChain }); - console.info('Snap installed!'); - return { isSnapInstalled: true, snap: metamaskPolkadotSnap }; - } catch (e) { - console.error(e); - return { isSnapInstalled: false }; - } -} -export interface ExtensionAccount { - address: string; - name: string; - source: string; - signer: Signer; -} - -export async function buildWalletAccount(extAcc: ExtensionAccount) { - return { - address: extAcc.address, - source: extAcc.source, - name: extAcc.name, - signer: extAcc.signer as WalletAccount['signer'], - wallet: { - enable: () => undefined, - extensionName: 'polkadot-js', - title: 'Metamask Wallet', - installUrl: 'https://metamask.io/', - installed: true, - extension: undefined, - signer: extAcc.signer, - logo: { - src: '', - alt: 'Metamask Wallet', - }, - /** - * The following methods are tagged as 'Unused' since they are only required by the @talisman package, - * which we are not using to handle this wallet connection. - */ - getAccounts: () => Promise.resolve([]), // Unused - subscribeAccounts: () => undefined, // Unused - transformError: (err: Error) => err, // Unused - }, - }; -} - -export async function initiateMetamaskInjectedAccount(tenantName: TenantName) { - const provider = await initiatePolkadotSnap(tenantToSnapNetwork(tenantName)); - if (!provider.isSnapInstalled) { - const installResult = await installPolkadotSnap(tenantToSnapNetwork(tenantName)); - if (!installResult) { - console.error('Something went wrong, snap could not be installed.'); - return; - } - } - if (provider.snap) { - const api = provider.snap.getMetamaskSnapApi(); - const injectedMetamaskAccount: ExtensionAccount = { - address: await api.getAddress(), - name: 'Metamask Snap', - source: WALLET_SOURCE_METAMASK, - signer: { - signPayload: async (payload: SignerPayloadJSON) => { - // TODO casting payload as any is only temporary, we need to fix the types - const stringResult = await api.signPayloadJSON(payload as any); - // Metamask snap doesn't provide a request Id, but just the hex string, so - // adding id: 1 to be compliant with SignerResult - return { id: 1, signature: stringResult } as SignerResult; - }, - signRaw: async (raw: SignerPayloadRaw) => { - const stringResult = await api.signPayloadRaw(raw); - // Metamask snap doesn't provide a request Id, but just the hex string, so - // adding id: 1 to be compliant with SignerResult - return { id: 1, signature: stringResult } as SignerResult; - }, - update: (id, status) => { - console.log('Status update for Id %d: %s', id, status.toHuman()); - }, - }, - }; - return await buildWalletAccount(injectedMetamaskAccount); - } - return undefined; -} diff --git a/src/services/nabla.ts b/src/services/nabla.ts index 5776fb7f..fc3239b4 100644 --- a/src/services/nabla.ts +++ b/src/services/nabla.ts @@ -7,141 +7,153 @@ import { getApiManagerInstance } from './polkadot/polkadotApi'; import { erc20WrapperAbi } from '../contracts/ERC20Wrapper'; import { routerAbi } from '../contracts/Router'; import { NABLA_ROUTER } from '../constants/constants'; -import { defaultReadLimits, multiplyByPowerOfTen } from '../helpers/contracts'; +import { defaultReadLimits, multiplyByPowerOfTen, stringifyBigWithSignificantDecimals } from '../helpers/contracts'; import { parseContractBalanceResponse } from '../helpers/contracts'; -import { TOKEN_CONFIG, TokenType } from '../constants/tokenConfig'; -import { WalletAccount } from '@talismn/connect-wallets'; +import { INPUT_TOKEN_CONFIG, OUTPUT_TOKEN_CONFIG, getPendulumCurrencyId } from '../constants/tokenConfig'; import { defaultWriteLimits, createWriteOptions } from '../helpers/contracts'; -import { toBigNumber } from '../helpers/parseNumbers'; +import { ExecutionContext, OfframpingState } from './offrampingFlow'; import { Keyring } from '@polkadot/api'; -import { getEphemeralAccount } from './polkadot/ephemeral'; -export interface PerformSwapProps { - amountInRaw: Big; - assetOut: string; - assetIn: string; - minAmountOut: Big; -} +// Since this operation reads first from chain the current approval, there is no need to +// save any state for potential recovery. +export async function nablaApprove( + state: OfframpingState, + { renderEvent }: ExecutionContext, +): Promise { + const { inputTokenType, inputAmountNabla, pendulumEphemeralSeed } = state; -export async function performSwap( - { amountInRaw, assetOut, assetIn, minAmountOut }: PerformSwapProps, - renderEvent: (event: string, status: EventStatus) => void, -): Promise { // event attempting swap - const assetInDetails = TOKEN_CONFIG[assetIn as TokenType]; - const assetOutDetails = TOKEN_CONFIG[assetOut as TokenType]; - - const amountIn = toBigNumber(amountInRaw, assetInDetails.decimals); + const inputToken = INPUT_TOKEN_CONFIG[inputTokenType]; - renderEvent('Attempting swap', EventStatus.Waiting); - console.log('swap', 'Attempting swap', amountIn, assetOut, assetIn, minAmountOut); + console.log('swap', 'Attempting swap', inputAmountNabla.units, inputTokenType); // get chain api, abi - const pendulumApiComponents = (await getApiManagerInstance()).apiData!; - const erc20ContractAbi = new Abi(erc20WrapperAbi, pendulumApiComponents.api.registry.getChainProperties()); - const routerAbiObject = new Abi(routerAbi, pendulumApiComponents.api.registry.getChainProperties()); + const { ss58Format, api } = (await getApiManagerInstance()).apiData!; + const erc20ContractAbi = new Abi(erc20WrapperAbi, api.registry.getChainProperties()); // get asset details // get ephermal keypair and account - const keypairEphemeral = getEphemeralAccount(); + const keyring = new Keyring({ type: 'sr25519', ss58Format }); + const ephemeralKeypair = keyring.addFromUri(pendulumEphemeralSeed); // call the current allowance of the ephemeral const response: ReadMessageResult = await readMessage({ abi: erc20ContractAbi, - api: pendulumApiComponents.api, - contractDeploymentAddress: assetInDetails.erc20Address!, - callerAddress: keypairEphemeral.address, + api: api, + contractDeploymentAddress: inputToken.axelarEquivalent.pendulumErc20WrapperAddress, + callerAddress: ephemeralKeypair.address, messageName: 'allowance', - messageArguments: [keypairEphemeral.address, NABLA_ROUTER], + messageArguments: [ephemeralKeypair.address, NABLA_ROUTER], limits: defaultReadLimits, }); if (response.type !== 'success') { const message = 'Could not load token allowance'; renderEvent(message, EventStatus.Error); - return Promise.reject(message); + throw new Error(message); } - const currentAllowance = parseContractBalanceResponse(assetInDetails.decimals, response.value); - - // Probably no need to multiply by power of ten here since amountIn comes from the event - //const rawAmountToSwapBig = multiplyByPowerOfTen(amountIn, assetInDetails.decimals); - const rawAmountToSwapBig = amountInRaw; - const rawAmountMinBig = multiplyByPowerOfTen(minAmountOut, assetOutDetails.decimals); + const currentAllowance = parseContractBalanceResponse(inputToken.decimals, response.value); //maybe do allowance - if (currentAllowance !== undefined && currentAllowance.rawBalance.lt(rawAmountToSwapBig)) { + if (currentAllowance === undefined || currentAllowance.rawBalance.lt(Big(inputAmountNabla.raw))) { try { renderEvent( - `Approving tokens: ${toBigNumber( - rawAmountToSwapBig, - assetInDetails.decimals, - )} ${assetInDetails.assetCode.toUpperCase()}`, + `Approving tokens: ${inputAmountNabla.units} ${inputToken.axelarEquivalent.pendulumAssetSymbol}`, EventStatus.Waiting, ); await approve({ - api: pendulumApiComponents.api, - amount: rawAmountToSwapBig.toFixed(), // toString can render exponential notation - token: assetInDetails.erc20Address!, + api: api, + amount: inputAmountNabla.raw, + token: inputToken.axelarEquivalent.pendulumErc20WrapperAddress, spender: NABLA_ROUTER, contractAbi: erc20ContractAbi, - keypairEphemeral, + keypairEphemeral: ephemeralKeypair, }); } catch (e) { renderEvent(`Could not approve token: ${e}`, EventStatus.Error); return Promise.reject('Could not approve token'); } } - // balance before the swap - const responseBalanceBefore = ( - await pendulumApiComponents.api.query.tokens.accounts(keypairEphemeral.address, assetOutDetails.currencyId) - ).toHuman() as any; - const rawBalanceBefore = responseBalanceBefore?.free || '0'; - const balanceBeforeBigDecimal = toBigNumber(rawBalanceBefore, assetOutDetails.decimals); + return { + ...state, + phase: 'nablaSwap', + }; +} - // Try swap - try { - //TODO amountIN has all zeroes now, need to fix the message. - renderEvent( - `Swapping ${amountIn} ${assetInDetails.assetCode.toUpperCase()} to ${minAmountOut} ${assetOutDetails.assetCode.toUpperCase()} `, - EventStatus.Waiting, - ); - await doActualSwap({ - api: pendulumApiComponents.api, - amount: rawAmountToSwapBig.toFixed(), // toString can render exponential notation - amountMin: rawAmountMinBig.toFixed(), // toString can render exponential notation - tokenIn: assetInDetails.erc20Address!, - tokenOut: assetOutDetails.erc20Address!, - contractAbi: routerAbiObject, - keypairEphemeral, - }); - } catch (e) { - let errorMessage = ''; - const result = (e as ExecuteMessageResult).result; - if (result.type === 'reverted') { - errorMessage = result.description; - } else if (result.type === 'error') { - errorMessage = result.error; - } else { - errorMessage = 'Something went wrong'; +export async function nablaSwap(state: OfframpingState, { renderEvent }: ExecutionContext): Promise { + const { inputTokenType, outputTokenType, inputAmountNabla, outputAmount, pendulumEphemeralSeed } = state; + + // event attempting swap + const inputToken = INPUT_TOKEN_CONFIG[inputTokenType]; + const outputToken = OUTPUT_TOKEN_CONFIG[outputTokenType]; + + // get chain api, abi + const { ss58Format, api } = (await getApiManagerInstance()).apiData!; + const routerAbiObject = new Abi(routerAbi, api.registry.getChainProperties()); + // get asset details + + // get ephermal keypair and account + const keyring = new Keyring({ type: 'sr25519', ss58Format }); + const ephemeralKeypair = keyring.addFromUri(pendulumEphemeralSeed); + + // balance before the swap. Important for recovery process. + // if transaction was able to get in, but we failed on the listening + const outputCurrencyId = getPendulumCurrencyId(outputTokenType); + const responseBalanceBefore = (await api.query.tokens.accounts(ephemeralKeypair.address, outputCurrencyId)) as any; + const rawBalanceBefore = Big(responseBalanceBefore?.free?.toString() ?? '0'); + + // Since this is an ephemeral account, balanceBefore being greater than the minimum amount means that the swap was successful + // but we missed the event. This is important for recovery process. + if (rawBalanceBefore.lt(Big(outputAmount.raw))) { + // Try swap + try { + renderEvent( + `Swapping ${inputAmountNabla.units} ${inputToken.axelarEquivalent.pendulumAssetSymbol} to ${outputAmount.units} ${outputToken.stellarAsset.code.string} `, + EventStatus.Waiting, + ); + + await doActualSwap({ + api: api, + amount: inputAmountNabla.raw, // toString can render exponential notation + amountMin: outputAmount.raw, // toString can render exponential notation + tokenIn: inputToken.axelarEquivalent.pendulumErc20WrapperAddress, + tokenOut: outputToken.erc20WrapperAddress, + contractAbi: routerAbiObject, + keypairEphemeral: ephemeralKeypair, + }); + } catch (e) { + let errorMessage = ''; + const result = (e as ExecuteMessageResult).result; + if (result.type === 'reverted') { + errorMessage = result.description; + } else if (result.type === 'error') { + errorMessage = result.error; + } else { + errorMessage = 'Something went wrong'; + } + renderEvent(`Could not swap the required amount of token: ${errorMessage}`, EventStatus.Error); + return Promise.reject('Could not swap token'); } - renderEvent(`Could not swap the required amount of token: ${errorMessage}`, EventStatus.Error); - return Promise.reject('Could not swap token'); - } - //verify token balance before releasing this process. - const responseBalanceAfter = ( - await pendulumApiComponents.api.query.tokens.accounts(keypairEphemeral.address, assetOutDetails.currencyId) - ).toHuman() as any; + //verify token balance before releasing this process. + const responseBalanceAfter = (await api.query.tokens.accounts(ephemeralKeypair.address, outputCurrencyId)) as any; + const rawBalanceAfter = Big(responseBalanceAfter?.free?.toString() ?? '0'); - const rawBalanceAfter = responseBalanceAfter?.free || '0'; - const balanceAfterBigDecimal = toBigNumber(rawBalanceAfter, assetOutDetails.decimals); + const actualOfframpValueRaw = rawBalanceAfter.sub(rawBalanceBefore); - const actualOfframpValue = balanceAfterBigDecimal.sub(balanceBeforeBigDecimal); + const actualOfframpValue = multiplyByPowerOfTen(actualOfframpValueRaw, -outputToken.decimals); - renderEvent(`Swap successful. Amount received: ${actualOfframpValue}`, EventStatus.Success); + renderEvent( + `Swap successful. Amount received: ${stringifyBigWithSignificantDecimals(actualOfframpValue, 2)}`, + EventStatus.Success, + ); + } - return actualOfframpValue; + return { + ...state, + phase: 'executeSpacewalkRedeem', + }; } async function approve({ api, token, spender, amount, contractAbi, keypairEphemeral }: any) { diff --git a/src/services/offrampingFlow.ts b/src/services/offrampingFlow.ts new file mode 100644 index 00000000..3b29b0fe --- /dev/null +++ b/src/services/offrampingFlow.ts @@ -0,0 +1,197 @@ +import { Config } from 'wagmi'; +import { INPUT_TOKEN_CONFIG, InputTokenType, OUTPUT_TOKEN_CONFIG, OutputTokenType } from '../constants/tokenConfig'; +import { squidRouter } from './squidrouter/process'; +import { createPendulumEphemeralSeed, pendulumCleanup, pendulumFundEphemeral } from './polkadot/ephemeral'; +import { SepResult, createStellarEphemeralSecret } from './anchor'; +import Big from 'big.js'; +import { multiplyByPowerOfTen } from '../helpers/contracts'; +import { setUpAccountAndOperations, stellarCleanup, stellarCreateEphemeral, stellarOfframp } from './stellar'; +import { nablaApprove, nablaSwap } from './nabla'; +import { RenderEventHandler } from '../components/GenericEvent'; +import { executeSpacewalkRedeem } from './polkadot'; +import { fetchSigningServiceAccountId } from './signingService'; +import { Keypair } from 'stellar-sdk'; +import { storageService } from './storage/local'; + +export type OfframpingPhase = + | 'squidRouter' + | 'pendulumFundEphemeral' + | 'nablaApprove' + | 'nablaSwap' + | 'executeSpacewalkRedeem' + | 'pendulumCleanup' + | 'stellarOfframp' + | 'stellarCleanup'; + +export interface OfframpingState { + pendulumEphemeralSeed: string; + stellarEphemeralSecret: string; + + inputTokenType: InputTokenType; + outputTokenType: OutputTokenType; + + inputAmount: { + units: string; + raw: string; + }; + inputAmountNabla: { + units: string; + raw: string; + }; + outputAmount: { + units: string; + raw: string; + }; + + phase: OfframpingPhase; + + // phase squidRouter + squidRouterApproveHash?: `0x${string}`; + squidRouterSwapHash?: `0x${string}`; + + // nablaApprove + nablaApproveNonce: number; + + // nablaSwap + nablaSwapNonce: number; + + // executeSpacewalk + executeSpacewalkNonce: number; + + // stellarOfframp + stellarOfframpingTransaction: string; + + // stellarCleanup + stellarCleanupTransaction: string; +} + +export type StateTransitionFunction = ( + state: OfframpingState, + context: ExecutionContext, +) => Promise; + +const STATE_ADVANCEMENT_HANDLERS: Record = { + squidRouter, + pendulumFundEphemeral, + nablaApprove, + nablaSwap, + executeSpacewalkRedeem, + pendulumCleanup, + stellarOfframp, + stellarCleanup, +}; + +export interface ExecutionContext { + wagmiConfig: Config; + renderEvent: RenderEventHandler; +} + +const OFFRAMPING_STATE_LOCAL_STORAGE_KEY = 'offrampingState'; + +export interface InitiateStateArguments { + inputTokenType: InputTokenType; + outputTokenType: OutputTokenType; + amountIn: string; + nablaAmountInRaw: string; + amountOut: string; + sepResult: SepResult; +} + +export async function constructInitialState({ + inputTokenType, + outputTokenType, + amountIn, + nablaAmountInRaw, + amountOut, + sepResult, +}: InitiateStateArguments) { + const pendulumEphemeralSeed = await createPendulumEphemeralSeed(); + const stellarEphemeralSecret = createStellarEphemeralSecret(); + + const inputTokenDecimals = INPUT_TOKEN_CONFIG[inputTokenType].decimals; + const outputTokenDecimals = OUTPUT_TOKEN_CONFIG[outputTokenType].decimals; + + const inputAmountBig = Big(amountIn); + const inputAmountRaw = multiplyByPowerOfTen(inputAmountBig, inputTokenDecimals).toFixed(); + + const inputAmountNablaRawBig = Big(nablaAmountInRaw); + const inputAmountNablaUnits = multiplyByPowerOfTen(inputAmountNablaRawBig, -inputTokenDecimals).toFixed(); + + const outputAmountBig = Big(amountOut).round(2, 0); + const outputAmountRaw = multiplyByPowerOfTen(outputAmountBig, outputTokenDecimals).toFixed(); + + await stellarCreateEphemeral(stellarEphemeralSecret, outputTokenType); + const stellarFundingAccountId = await fetchSigningServiceAccountId(); + const stellarEphemeralKeypair = Keypair.fromSecret(stellarEphemeralSecret); + const { offrampingTransaction, mergeAccountTransaction } = await setUpAccountAndOperations( + stellarFundingAccountId, + stellarEphemeralKeypair, + sepResult, + outputTokenType, + ); + + const initialState: OfframpingState = { + pendulumEphemeralSeed, + stellarEphemeralSecret, + inputTokenType, + outputTokenType, + inputAmount: { + units: amountIn, + raw: inputAmountRaw, + }, + inputAmountNabla: { + units: inputAmountNablaUnits, + raw: nablaAmountInRaw, + }, + outputAmount: { + units: outputAmountBig.toFixed(2, 0), + raw: outputAmountRaw, + }, + phase: 'squidRouter', + nablaApproveNonce: 0, + nablaSwapNonce: 1, + executeSpacewalkNonce: 2, + + stellarOfframpingTransaction: offrampingTransaction.toEnvelope().toXDR().toString('base64'), + stellarCleanupTransaction: mergeAccountTransaction.toEnvelope().toXDR().toString('base64'), + }; + + storageService.set(OFFRAMPING_STATE_LOCAL_STORAGE_KEY, initialState); + return initialState; +} + +export async function advanceOfframpingState(context: ExecutionContext): Promise { + const state = storageService.getParsed(OFFRAMPING_STATE_LOCAL_STORAGE_KEY); + + if (state === undefined) { + console.log('No offramping in process'); + return undefined; + } + console.log('Advance offramping state in phase', state.phase); + + let newState: OfframpingState | undefined; + try { + newState = await STATE_ADVANCEMENT_HANDLERS[state.phase](state, context); + } catch (error) { + if ((error as any)?.message === 'Wallet not connected') { + // TODO: transmit error to caller + console.error('Wallet not connected. Try to connect wallet'); + return state; + } + console.error('Unrecoverable error advancing offramping state', error); + return undefined; + } + + if (newState !== undefined) { + storageService.set(OFFRAMPING_STATE_LOCAL_STORAGE_KEY, newState); + } else { + storageService.remove(OFFRAMPING_STATE_LOCAL_STORAGE_KEY); + } + + console.log('Done advancing offramping state and advance to', newState?.phase ?? 'completed'); + return newState; +} + +export function isOfframpOngoing(): boolean { + return storageService.getParsed(OFFRAMPING_STATE_LOCAL_STORAGE_KEY) !== undefined; +} diff --git a/src/services/polkadot/__tests__/spacewalk.test.tsx b/src/services/polkadot/__tests__/spacewalk.test.tsx index 858dc080..c68f5d4c 100644 --- a/src/services/polkadot/__tests__/spacewalk.test.tsx +++ b/src/services/polkadot/__tests__/spacewalk.test.tsx @@ -1,9 +1,8 @@ -import { WalletAccount } from '@talismn/connect-wallets'; import { Keypair } from 'stellar-sdk'; import { Keyring } from '@polkadot/api'; import { getApiManagerInstance } from '../polkadotApi'; import { getVaultsForCurrency, VaultService } from '../spacewalk'; -import { executeSpacewalkRedeem } from '../index'; +import { OUTPUT_TOKEN_CONFIG, OutputTokenType } from '../../../constants/tokenConfig'; // The secret phrase of a substrate account on Pendulum used for requesting a redeem const TEST_ACCOUNT_SECRET_PHRASE = process.env.TEST_ACCOUNT_SECRET_PHRASE || ''; @@ -32,7 +31,12 @@ async function setupTest() { const apiComponents = await apiManager.getApiComponents(); const api = apiComponents.api; - const vaultsForCurrency = await getVaultsForCurrency(api, TEST_CURRENCY_SYMBOL); + const testToken = OUTPUT_TOKEN_CONFIG[TEST_CURRENCY_SYMBOL.toLowerCase() as OutputTokenType]; + const vaultsForCurrency = await getVaultsForCurrency( + api, + testToken.stellarAsset.code.hex, + testToken.stellarAsset.issuer.hex, + ); if (vaultsForCurrency.length === 0) { console.log(`No vaults found for currency ${TEST_CURRENCY_SYMBOL}`); return; diff --git a/src/services/polkadot/ephemeral.tsx b/src/services/polkadot/ephemeral.tsx index d8340550..b0dd7266 100644 --- a/src/services/polkadot/ephemeral.tsx +++ b/src/services/polkadot/ephemeral.tsx @@ -1,132 +1,159 @@ import { Keyring } from '@polkadot/api'; -import { KeyringPair } from '@polkadot/keyring/types'; import { mnemonicGenerate } from '@polkadot/util-crypto'; import { getApiManagerInstance } from './polkadotApi'; -import { parseTokenDepositEvent, TokenTransferEvent } from './eventParsers'; -import { compareObjects } from './eventParsers'; -import { getAddressForFormat } from '../../helpers/addressFormatter'; -import { decimalToNative } from '../../helpers/parseNumbers'; -import { TokenType } from '../../constants/tokenConfig'; -import { TOKEN_CONFIG } from '../../constants/tokenConfig'; - -let fundingAccountKeypair: KeyringPair | null = null; -const FUNDING_AMOUNT = decimalToNative(0.1).toNumber(); // 0.1 PEN +import { getPendulumCurrencyId, INPUT_TOKEN_CONFIG, OUTPUT_TOKEN_CONFIG } from '../../constants/tokenConfig'; +import Big from 'big.js'; +import { ExecutionContext, OfframpingState } from '../offrampingFlow'; +import { waitForEvmTransaction } from '../evmTransactions'; +import { multiplyByPowerOfTen } from '../../helpers/contracts'; + +const FUNDING_AMOUNT_UNITS = '0.1'; + // TODO: replace const SEED_PHRASE = 'hood protect select grace number hurt lottery property stomach grit bamboo field'; -// print the public key using the correct ss58 format -async function printEphemeralAccount(seedPhrase: string) { - const { apiData } = await getApiManagerInstance(); - const keyring = new Keyring({ type: 'sr25519', ss58Format: apiData?.ss58Format }); - fundingAccountKeypair = keyring.addFromUri(seedPhrase); - console.log('Ephemeral account seedphrase: ', seedPhrase); - console.log('Ephemeral account created:', fundingAccountKeypair.address); -} - -export const getEphemeralAccount = () => { - if (!fundingAccountKeypair) { - const seedPhrase = mnemonicGenerate(); - const keyring = new Keyring({ type: 'sr25519' }); - fundingAccountKeypair = keyring.addFromUri(seedPhrase); - printEphemeralAccount(seedPhrase); +export async function pendulumFundEphemeral( + state: OfframpingState, + { wagmiConfig }: ExecutionContext, +): Promise { + console.log('Pendulum funding ephemeral account'); + const { squidRouterSwapHash, pendulumEphemeralSeed } = state; + if (squidRouterSwapHash === undefined) { + throw new Error('No squid router swap hash found'); } - return fundingAccountKeypair; -}; -export const fundEphemeralAccount = async () => { - try { + await waitForEvmTransaction(squidRouterSwapHash, wagmiConfig); + + const isAlreadyFunded = await isEphemeralFunded(state); + + if (!isAlreadyFunded) { const pendulumApiComponents = await getApiManagerInstance(); const apiData = pendulumApiComponents.apiData!; - const ephemeralAddress = getEphemeralAccount().address; - - const keyring = new Keyring({ type: 'sr25519' }); + const keyring = new Keyring({ type: 'sr25519', ss58Format: apiData.ss58Format }); + const ephemeralKeypair = keyring.addFromUri(pendulumEphemeralSeed); const fundingAccountKeypair = keyring.addFromUri(SEED_PHRASE); - await apiData.api.tx.balances.transfer(ephemeralAddress, FUNDING_AMOUNT).signAndSend(fundingAccountKeypair); - } catch (error) { - console.error('Error funding account', error); + const fundingAmountUnits = Big(FUNDING_AMOUNT_UNITS); + const fundingAmountRaw = multiplyByPowerOfTen(fundingAmountUnits, apiData.decimals).toFixed(); + + await apiData.api.tx.balances + .transfer(ephemeralKeypair.address, fundingAmountRaw) + .signAndSend(fundingAccountKeypair); + + await waitForPendulumEphemeralFunding(state); + } + + await waitForInputTokenToArrive(state); + + return { + ...state, + phase: 'nablaApprove', + }; +} + +async function isEphemeralFunded(state: OfframpingState) { + const { pendulumEphemeralSeed } = state; + const pendulumApiComponents = await getApiManagerInstance(); + const apiData = pendulumApiComponents.apiData!; + + const keyring = new Keyring({ type: 'sr25519', ss58Format: apiData.ss58Format }); + const ephemeralKeypair = keyring.addFromUri(pendulumEphemeralSeed); + + const fundingAmountUnits = Big(FUNDING_AMOUNT_UNITS); + const fundingAmountRaw = multiplyByPowerOfTen(fundingAmountUnits, apiData.decimals).toFixed(); + + const { data: balance } = await apiData.api.query.system.account(ephemeralKeypair.address); + + // check if balance is higher than minimum required, then we consider the account ready + return Big(balance.free.toString()).gte(fundingAmountRaw); +} + +async function waitForPendulumEphemeralFunding(state: OfframpingState) { + while (true) { + const isFunded = await isEphemeralFunded(state); + if (isFunded) return; + + await new Promise((resolve) => setTimeout(resolve, 1000)); } -}; +} + +async function waitForInputTokenToArrive(state: OfframpingState) { + console.log('Waiting for input token to arrive on pendulum'); + while (true) { + const isFunded = await didInputTokenArriveOnPendulum(state); + if (isFunded) { + console.log('Input token arrived on pendulum'); + return; + } + + await new Promise((resolve) => setTimeout(resolve, 1000)); + } +} -export const cleanEphemeralAccount = async (token: TokenType) => { +export async function createPendulumEphemeralSeed() { + const seedPhrase = mnemonicGenerate(); + + const pendulumApiComponents = await getApiManagerInstance(); + const apiData = pendulumApiComponents.apiData!; + const keyring = new Keyring({ type: 'sr25519', ss58Format: apiData.ss58Format }); + + const ephemeralAccountKeypair = keyring.addFromUri(seedPhrase); + console.log('Ephemeral account seedphrase: ', seedPhrase); + console.log('Ephemeral account created:', ephemeralAccountKeypair.address); + + return seedPhrase; +} + +export async function pendulumCleanup(state: OfframpingState): Promise { try { + const { pendulumEphemeralSeed, inputTokenType, outputTokenType } = state; + const inputToken = INPUT_TOKEN_CONFIG[inputTokenType]; + const outputToken = OUTPUT_TOKEN_CONFIG[outputTokenType]; + const pendulumApiComponents = await getApiManagerInstance(); - const apiData = pendulumApiComponents.apiData!; + const { api, ss58Format } = pendulumApiComponents.apiData!; - const ephemeralKeyring = getEphemeralAccount(); - const ephemeralAddress = getAddressForFormat(ephemeralKeyring.address, apiData.ss58Format); - - const keyring = new Keyring({ type: 'sr25519' }); - const fundingAccountKeypair = keyring.addFromUri(SEED_PHRASE); - const fundingAccountAddress = getAddressForFormat(fundingAccountKeypair.address, apiData.ss58Format); + const keyring = new Keyring({ type: 'sr25519', ss58Format }); + const ephemeralKeypair = keyring.addFromUri(pendulumEphemeralSeed); + const fundingAccountAddress = keyring.addFromUri(SEED_PHRASE).address; // probably will never be exactly '0', but to be safe // TODO: if the value is too small, do we really want to transfer token dust and spend fees? - await apiData.api.tx.tokens.transferAll(fundingAccountAddress, TOKEN_CONFIG[token].currencyId, false).signAndSend(ephemeralKeyring); - - await apiData.api.tx.balances.transferAll(fundingAccountAddress, false).signAndSend(ephemeralKeyring); + const inputCurrencyId = inputToken.axelarEquivalent.pendulumCurrencyId; + const outputCurrencyId = getPendulumCurrencyId(outputTokenType); + + await api.tx.utility + .batchAll([ + api.tx.tokens.transferAll(fundingAccountAddress, inputCurrencyId, false), + api.tx.tokens.transferAll(fundingAccountAddress, outputCurrencyId, false), + api.tx.balances.transferAll(fundingAccountAddress, false), + ]) + .signAndSend(ephemeralKeypair); } catch (error) { console.error('Error cleaning pendulum ephemeral account', error); } -}; -// function to check balance of account, native token -export async function checkBalance(): Promise { - const pendulumApiComponents = await getApiManagerInstance(); - if (!fundingAccountKeypair) { - return false; - } - const { data: balance } = await pendulumApiComponents.apiData!.api.query.system.account( - fundingAccountKeypair?.address, - ); - - // check if balance is higher than minimum required, then we consider the account ready - return balance.free.toNumber() >= FUNDING_AMOUNT; + return { ...state, phase: 'stellarOfframp' }; } -export async function waitForTokenReceptionEvent( - expectedCurrencyId: any, - maxWaitingTimeMs: number, -): Promise { +async function didInputTokenArriveOnPendulum({ + inputAmountNabla, + pendulumEphemeralSeed, + inputTokenType, +}: OfframpingState): Promise { const pendulumApiComponents = await getApiManagerInstance(); - const ephemeralAddress = getEphemeralAccount().address; - const apiData = pendulumApiComponents.apiData!; + const { api, ss58Format } = pendulumApiComponents.apiData!; - const filter = (event: any) => { - if (event.event.section === 'tokens' && event.event.method === 'Deposited') { - console.log('Deposit Event:', event); - const eventParsed = parseTokenDepositEvent(event); - if (eventParsed.to != getAddressForFormat(ephemeralAddress, apiData.ss58Format)) { - return null; - } - if (compareObjects(eventParsed.currencyId, expectedCurrencyId)) { - return eventParsed; - } - } - return null; - }; + const keyring = new Keyring({ type: 'sr25519', ss58Format }); + const ephemeralKeypair = keyring.addFromUri(pendulumEphemeralSeed); + const inputToken = INPUT_TOKEN_CONFIG[inputTokenType]; + + const balanceResponse = (await api.query.tokens.accounts( + ephemeralKeypair.address, + inputToken.axelarEquivalent.pendulumCurrencyId, + )) as any; + const inputBalanceRaw = Big(balanceResponse?.free?.toString() ?? '0'); - return new Promise((resolve, reject) => { - let unsubscribeFromEventsPromise: Promise<() => void> | null = null; - const timeout = setTimeout(() => { - if (unsubscribeFromEventsPromise) { - unsubscribeFromEventsPromise.then((unsubscribe) => unsubscribe()); - } - reject(new Error(`Max waiting time exceeded for token reception`)); - }, maxWaitingTimeMs); - - unsubscribeFromEventsPromise = apiData.api.query.system.events((events) => { - events.forEach((event) => { - const eventParsed = filter(event); - if (eventParsed) { - if (unsubscribeFromEventsPromise) { - unsubscribeFromEventsPromise.then((unsubscribe) => unsubscribe()); - } - clearTimeout(timeout); - resolve(eventParsed); - } - }); - }); - }); + return inputBalanceRaw.gte(Big(inputAmountNabla.raw)); } diff --git a/src/services/polkadot/eventListener.tsx b/src/services/polkadot/eventListener.tsx index c8e35d2c..bf4fbda3 100644 --- a/src/services/polkadot/eventListener.tsx +++ b/src/services/polkadot/eventListener.tsx @@ -7,7 +7,7 @@ interface IPendingEvent { } export class EventListener { - static eventListeners = new Map(); + static eventListeners = new Map(); pendingIssueEvents: IPendingEvent[] = []; pendingRedeemEvents: IPendingEvent[] = []; @@ -20,11 +20,12 @@ export class EventListener { } static getEventListener(api: ApiPromise) { - if (!this.eventListeners.has(api)) { - const newListener = new EventListener(api); - this.eventListeners.set(api, newListener); - } - return this.eventListeners.get(api); + const eventListener = this.eventListeners.get(api); + if (eventListener) return eventListener; + + const newListener = new EventListener(api); + this.eventListeners.set(api, newListener); + return newListener; } async initEventSubscriber() { diff --git a/src/services/polkadot/index.tsx b/src/services/polkadot/index.tsx index 2a3c2c0d..1c677980 100644 --- a/src/services/polkadot/index.tsx +++ b/src/services/polkadot/index.tsx @@ -2,76 +2,156 @@ import { Keypair } from 'stellar-sdk'; import { ApiManager } from './polkadotApi'; import { getVaultsForCurrency, VaultService } from './spacewalk'; import { prettyPrintVaultId } from './spacewalk'; -import { stringDecimalToStellarNative } from '../../helpers/parseNumbers'; import { EventListener } from './eventListener'; import { EventStatus } from '../../components/GenericEvent'; -import { WalletAccount } from '@talismn/connect-wallets'; -import { KeyringPair } from '@polkadot/keyring/types'; -import { TokenDetails } from '../../constants/tokenConfig'; +import { OUTPUT_TOKEN_CONFIG, OutputTokenDetails } from '../../constants/tokenConfig'; +import { getStellarBalanceUnits } from '../stellar/utils'; +import Big from 'big.js'; +import { ExecutionContext, OfframpingState } from '../offrampingFlow'; +import { Keyring } from '@polkadot/api'; export async function executeSpacewalkRedeem( - stellarTargetAccountId: string, - amountString: string, - accountOrPair: WalletAccount | KeyringPair, - tokenConfig: TokenDetails, - renderEvent: (event: string, status: EventStatus) => void, -) { + state: OfframpingState, + { renderEvent }: ExecutionContext, +): Promise { + const { outputAmount, stellarEphemeralSecret, pendulumEphemeralSeed, outputTokenType } = state; + const outputToken = OUTPUT_TOKEN_CONFIG[outputTokenType]; + const pendulumApiComponents = await new ApiManager().getApiComponents(); - // Query all available vaults for the currency - // we give priority again to the hex string, since we know the vault will match against this value - // in case the asset is represented as this if the asset is 3 letter. - const assetCodeOrHex = tokenConfig.assetCodeHex || tokenConfig.assetCode; + const { ss58Format, api } = pendulumApiComponents; + + // get ephermal keypair and account + const keyring = new Keyring({ type: 'sr25519', ss58Format }); + const ephemeralKeypair = keyring.addFromUri(pendulumEphemeralSeed); + + const stellarEphemeralKeypair = Keypair.fromSecret(stellarEphemeralSecret); + const stellarTargetAccountId = stellarEphemeralKeypair.publicKey(); + + // We wait for up to 10 minutes + const maxWaitingTimeMinutes = 10; + const maxWaitingTimeMs = maxWaitingTimeMinutes * 60 * 1000; + const stellarPollingTimeMs = 1 * 1000; // One of these two values must exist - const vaultsForCurrency = await getVaultsForCurrency(pendulumApiComponents.api, assetCodeOrHex!); + const vaultsForCurrency = await getVaultsForCurrency( + api, + outputToken.stellarAsset.code.hex, + outputToken.stellarAsset.issuer.hex, + ); if (vaultsForCurrency.length === 0) { - throw new Error(`No vaults found for currency ${assetCodeOrHex}`); + throw new Error(`No vaults found for currency ${outputToken.stellarAsset.code.string}`); } const targetVaultId = vaultsForCurrency[0].id; const vaultService = new VaultService(targetVaultId, pendulumApiComponents); - // We currently charge 0 fees for redeem requests on Spacewalk so the amount is the same as the requested amount - const amountRaw = stringDecimalToStellarNative(amountString).toString(); + const amountUnitsBig = new Big(outputAmount.units); // Generate raw public key for target const stellarTargetKeypair = Keypair.fromPublicKey(stellarTargetAccountId); const stellarTargetAccountIdRaw = stellarTargetKeypair.rawPublicKey(); - let redeemRequestEvent; - try { - renderEvent( - `Requesting redeem of ${amountRaw} tokens for vault ${prettyPrintVaultId(targetVaultId)}`, - EventStatus.Waiting, - ); - redeemRequestEvent = await vaultService.requestRedeem(accountOrPair, amountRaw, stellarTargetAccountIdRaw); - } catch (error) { - // Generic failure of the extrinsic itself OR lack of funds to even make the transaction - renderEvent(`Failed to request redeem: ${error}`, EventStatus.Error); - console.log(`Failed to request redeem: ${error}`); - throw new Error(`Failed to request redeem`); - } + // Recovery guard. If the operation was shut before the redeem was executed (we didn't register the event) we can + // avoid sending it again. + // We check for stellar funds. + const someBalanceUnits = await getStellarBalanceUnits(stellarTargetAccountId, outputToken.stellarAsset.code.string); + if (someBalanceUnits.lt(amountUnitsBig)) { + let redeemRequestEvent; - console.log( - `Successfully posed redeem request ${redeemRequestEvent.redeemId} for vault ${prettyPrintVaultId(targetVaultId)}`, - ); - //Render event that the extrinsic passed, and we are now waiting for the execution of it + try { + renderEvent( + `Requesting redeem of ${outputAmount.units} tokens for vault ${prettyPrintVaultId(targetVaultId)}`, + EventStatus.Waiting, + ); + redeemRequestEvent = await vaultService.requestRedeem( + ephemeralKeypair, + outputAmount.raw, + stellarTargetAccountIdRaw, + ); - const eventListener = EventListener.getEventListener(pendulumApiComponents.api); - // We wait for up to 5 minutes - const maxWaitingTimeMin = 5; - const maxWaitingTimeMs = maxWaitingTimeMin * 60 * 1000; + console.log( + `Successfully posed redeem request ${redeemRequestEvent.redeemId} for vault ${prettyPrintVaultId( + targetVaultId, + )}`, + ); + //Render event that the extrinsic passed, and we are now waiting for the execution of it - renderEvent( - `Redeem request passed, waiting up to ${maxWaitingTimeMin} minutes for redeem execution event...`, - EventStatus.Waiting, - ); + const eventListener = EventListener.getEventListener(pendulumApiComponents.api); + + renderEvent( + `Redeem request passed, waiting up to ${maxWaitingTimeMinutes} minutes for redeem execution event...`, + EventStatus.Waiting, + ); - try { - const redeemEvent = await eventListener.waitForRedeemExecuteEvent(redeemRequestEvent.redeemId, maxWaitingTimeMs); - } catch (error) { - // This is a potentially recoverable error (due to network delay) - // in the future we should distinguish between recoverable and non-recoverable errors - console.log(`Failed to wait for redeem execution: ${error}`); - renderEvent(`Failed to wait for redeem execution: Max waiting time exceeded`, EventStatus.Error); - throw new Error(`Failed to wait for redeem execution`); + try { + await eventListener.waitForRedeemExecuteEvent(redeemRequestEvent.redeemId, maxWaitingTimeMs); + } catch (error) { + // This is a potentially recoverable error (due to network delay) + // in the future we should distinguish between recoverable and non-recoverable errors + console.log(`Failed to wait for redeem execution: ${error}`); + renderEvent(`Failed to wait for redeem execution: Max waiting time exceeded`, EventStatus.Error); + throw new Error(`Failed to wait for redeem execution`); + } + } catch (error) { + // This is a potentially recoverable error (due to redeem request done before app shut down, but not registered) + if ((error as any).message.includes('AmountExceedsUserBalance')) { + console.log(`Recovery mode: Redeem already performed. Waiting for execution and Stellar balance arrival.`); + try { + await checkBalancePeriodically( + stellarTargetAccountId, + outputToken, + amountUnitsBig, + stellarPollingTimeMs, + maxWaitingTimeMs, + ); + console.log('Balance check completed successfully.'); + } catch (balanceCheckError) { + throw new Error(`Stellar balance did not arrive on time`); + } + } else { + // Generic failure of the extrinsic itself OR lack of funds to even make the transaction + console.log(`Failed to request redeem: ${error}`); + throw new Error(`Failed to request redeem`); + } + } } + + renderEvent('Redeem process completed, executing offramp transaction', EventStatus.Waiting); + return { ...state, phase: 'pendulumCleanup' }; +} + +function checkBalancePeriodically( + stellarTargetAccountId: string, + outputToken: OutputTokenDetails, + amountDesiredUnitsBig: Big, + intervalMs: number, + timeoutMs: number, +) { + return new Promise((resolve, reject) => { + const startTime = Date.now(); + const intervalId = setInterval(async () => { + try { + const someBalanceUnits = await getStellarBalanceUnits( + stellarTargetAccountId, + outputToken.stellarAsset.code.string, + ); + console.log(`Balance check: ${someBalanceUnits.toString()} / ${amountDesiredUnitsBig.toString()}`); + + if (someBalanceUnits.gte(amountDesiredUnitsBig)) { + clearInterval(intervalId); + resolve(someBalanceUnits); + } else if (Date.now() - startTime > timeoutMs) { + clearInterval(intervalId); + reject(new Error(`Balance did not meet the limit within the specified time (${timeoutMs} ms)`)); + } + } catch (error) { + console.error('Error checking balance:', error); + // Don't clear the interval here, allow it to continue checking + } + }, intervalMs); + + // Set a timeout to reject the promise if the total time exceeds timeoutMs + setTimeout(() => { + clearInterval(intervalId); + reject(new Error(`Balance did not meet the limit within the specified time (${timeoutMs} ms)`)); + }, timeoutMs); + }); } diff --git a/src/services/polkadot/polkadotApi.tsx b/src/services/polkadot/polkadotApi.tsx index f5d22c2d..3d80a58d 100644 --- a/src/services/polkadot/polkadotApi.tsx +++ b/src/services/polkadot/polkadotApi.tsx @@ -6,6 +6,7 @@ export interface ApiComponents { api: ApiPromise; mutex: Mutex; ss58Format: number; + decimals: number; } class ApiManager { @@ -20,9 +21,10 @@ class ApiManager { const mutex = new Mutex(); const chainProperties = api.registry.getChainProperties(); - const ss58Format = Number(chainProperties?.get('ss58Format').toString() || 42); + const ss58Format = Number(chainProperties?.get('ss58Format')?.toString() ?? 42); + const decimals = Number(chainProperties?.get('tokenDecimals')?.toHuman()[0]) ?? 12; - return { api, mutex, ss58Format }; + return { api, mutex, ss58Format, decimals }; } async populateApi() { @@ -30,6 +32,7 @@ class ApiManager { console.log(`Connecting to node ${network.wss}...`); this.apiData = await this.connectApi(network.wss); + await this.apiData.api.isReady; console.log(`Connected to node ${network.wss}`); } @@ -70,8 +73,9 @@ let instance: ApiManager | undefined = undefined; export async function getApiManagerInstance(): Promise { if (!instance) { - instance = new ApiManager(); - await instance.populateApi(); + let instancePreparing = new ApiManager(); + await instancePreparing.populateApi(); + instance = instancePreparing; } return instance; } diff --git a/src/services/polkadot/spacewalk.tsx b/src/services/polkadot/spacewalk.tsx index 85464933..18d990f2 100644 --- a/src/services/polkadot/spacewalk.tsx +++ b/src/services/polkadot/spacewalk.tsx @@ -71,15 +71,18 @@ function prettyPrintAssetInfo(assetInfo: any) { return assetInfo.code; } -export async function getVaultsForCurrency(api: ApiPromise, currencySymbol: string) { +export async function getVaultsForCurrency(api: ApiPromise, assetCodeHex: string, assetIssuerHex: string) { const vaultEntries = await api.query.vaultRegistry.vaults.entries(); const vaults = vaultEntries.map(([key, value]) => value.unwrap()); const vaultsForCurrency = vaults.filter((vault) => { + // toString returns the hex string + // toHuman returns the hex string if the string has length < 4, otherwise the readable string return ( vault.id.currencies.wrapped.isStellar && vault.id.currencies.wrapped.asStellar.isAlphaNum4 && - vault.id.currencies.wrapped.asStellar.asAlphaNum4.code.toHuman() === currencySymbol + vault.id.currencies.wrapped.asStellar.asAlphaNum4.code.toString() === assetCodeHex && + vault.id.currencies.wrapped.asStellar.asAlphaNum4.issuer.toString() === assetIssuerHex ); }); @@ -101,7 +104,7 @@ export class VaultService { this.apiComponents = apiComponents; } - async requestRedeem(accountOrPair: WalletAccount | KeyringPair, amount: string, stellarPkBytesBuffer: Buffer) { + async requestRedeem(accountOrPair: WalletAccount | KeyringPair, amountRaw: string, stellarPkBytesBuffer: Buffer) { const keyring = new Keyring({ type: 'sr25519' }); keyring.setSS58Format(this.apiComponents!.ss58Format); @@ -119,14 +122,15 @@ export class VaultService { const stellarPkBytes = Uint8Array.from(stellarPkBytesBuffer); return new Promise((resolve, reject) => - this.apiComponents!.api.tx.redeem.requestRedeem(amount, stellarPkBytes, this.vaultId!) - //Should we specify the nonce or is the wallet taking care of this? + this.apiComponents!.api.tx.redeem.requestRedeem(amountRaw, stellarPkBytes, this.vaultId!) .signAndSend(addressOrPair, options, (submissionResult: ISubmittableResult) => { const { status, events, dispatchError } = submissionResult; if (status.isFinalized) { console.log( - `Requested redeem of ${amount} for vault ${prettyPrintVaultId(this.vaultId)} with status ${status.type}`, + `Requested redeem of ${amountRaw} for vault ${prettyPrintVaultId(this.vaultId)} with status ${ + status.type + }`, ); // Try to find a 'system.ExtrinsicFailed' event diff --git a/src/services/polkadot/utils.tsx b/src/services/polkadot/utils.tsx deleted file mode 100644 index 812cc86f..00000000 --- a/src/services/polkadot/utils.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { ApiManager } from './polkadotApi'; -import { Keyring } from '@polkadot/api'; - -export const checkPendulumAccount = async (secret: string): Promise => { - const apiManager = new ApiManager(); - const { api, ss58Format } = await apiManager.getApiComponents(); - - try { - const keyring = new Keyring({ type: 'sr25519' }); - keyring.setSS58Format(ss58Format); - const origin = keyring.addFromUri(secret); - const { data } = await api.query.system.account(origin.address); - const freeBalance = data.free.toBigInt(); - if (freeBalance > 0) { - return true; - } - return false; - } catch (error) { - console.error('Pendulum Account Error:', error); - return false; - } -}; diff --git a/src/services/signingService.tsx b/src/services/signingService.tsx index 361c90df..79d031cc 100644 --- a/src/services/signingService.tsx +++ b/src/services/signingService.tsx @@ -5,7 +5,7 @@ interface SigningServiceStatus { public: string; } -export const fetchSigningServicePK = async (): Promise => { +export const fetchSigningServiceAccountId = async (): Promise => { try { const serviceResponse: SigningServiceStatus = await (await fetch(`${SIGNING_SERVICE_URL}/v1/status`)).json(); diff --git a/src/services/squidrouter/config.ts b/src/services/squidrouter/config.ts index af385d6e..fd8a9321 100644 --- a/src/services/squidrouter/config.ts +++ b/src/services/squidrouter/config.ts @@ -1,4 +1,4 @@ -import { TOKEN_CONFIG } from '../../constants/tokenConfig'; +import { INPUT_TOKEN_CONFIG, InputTokenDetails } from '../../constants/tokenConfig'; interface Config { fromChainId: string; @@ -9,11 +9,11 @@ interface Config { receivingContractAddress: string; } -export function getSquidRouterConfig(): Config { +export function getSquidRouterConfig(inputToken: InputTokenDetails): Config { return { fromChainId: '137', toChainId: '1284', - fromToken: TOKEN_CONFIG.usdc.erc20AddressNativeChain as `0x${string}`, + fromToken: inputToken.erc20AddressSourceChain as `0x${string}`, axlUSDC_MOONBEAM: '0xca01a1d0993565291051daff390892518acfad3a', integratorId: 'pendulum-7cffebc5-f84f-4669-96b4-4f8c82640811', receivingContractAddress: '0x066d12e8f155c87a87d9db96eac0594e872c16b2', diff --git a/src/services/squidrouter/index.tsx b/src/services/squidrouter/index.tsx deleted file mode 100644 index 8fbebe9b..00000000 --- a/src/services/squidrouter/index.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import { useAccount, useSendTransaction, useWaitForTransactionReceipt, useWriteContract } from 'wagmi'; -import { useCallback, useEffect, useState } from 'preact/compat'; -import { getRouteTransactionRequest } from './route'; -import erc20ABI from '../../contracts/ERC20'; -import { getSquidRouterConfig } from './config'; - -function useApproveSpending( - transactionRequestTarget: string | undefined, - fromToken: `0x${string}`, - fromAmount: string, -) { - const { data: hash, error, isPending, writeContract } = useWriteContract(); - const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({ - hash, - }); - - const approveSpending = useCallback(async () => { - console.log('Asking for approval of', transactionRequestTarget, fromToken, fromAmount); - - writeContract({ - abi: erc20ABI, - address: fromToken, - functionName: 'approve', - args: [transactionRequestTarget, fromAmount], - }); - }, [fromToken, fromAmount, transactionRequestTarget, writeContract]); - - return { - approveSpending, - error, - isPending, - isConfirming, - isConfirmed, - }; -} - -function useSendSwapTransaction(transactionRequest: any) { - const { data: hash, isPending, error, status, sendTransaction } = useSendTransaction(); - const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({ hash: hash }); - - const sendSwapTransaction = useCallback(async () => { - if (!transactionRequest) { - console.error('No transaction request found'); - return; - } - - console.log('Sending swap transaction'); - - // Execute the swap transaction - sendTransaction({ - to: transactionRequest.target, - data: transactionRequest.data, - value: transactionRequest.value, - gas: BigInt(transactionRequest.gasLimit) * BigInt(2), - }); - }, [transactionRequest, sendTransaction]); - - return { - hash, - error, - sendSwapTransaction, - isConfirming, - isConfirmed, - }; -} - -export enum TransactionStatus { - Idle = 'Idle', - RouteRequested = 'RouteRequested', - ApproveSpending = 'ApproveSpending', - SpendingApproved = 'SpendingApproved', - InitiateSwap = 'InitiateSwap', - SwapCompleted = 'SwapCompleted', -} - -export function useSquidRouterSwap(amount: string) { - const { fromToken } = getSquidRouterConfig(); - - const [requestId, setRequestId] = useState(''); - const [transactionRequest, setTransactionRequest] = useState(); - const [transactionStatus, setTransactionStatus] = useState(TransactionStatus.Idle); - - const accountData = useAccount(); - - const { - approveSpending, - isConfirming: isApprovalConfirming, - isConfirmed: isSpendingApproved, - error: approveError, - } = useApproveSpending(transactionRequest?.target, fromToken, amount); - - const { - hash, - isConfirming: isSwapConfirming, - isConfirmed: isSwapCompleted, - sendSwapTransaction, - error: swapError, - } = useSendSwapTransaction(transactionRequest); - // Update the transaction status - useEffect(() => { - if (isApprovalConfirming) { - setTransactionStatus(TransactionStatus.ApproveSpending); - } else if (isSpendingApproved) { - setTransactionStatus(TransactionStatus.SpendingApproved); - } else if (isSwapConfirming) { - setTransactionStatus(TransactionStatus.InitiateSwap); - } else if (isSwapCompleted) { - setTransactionStatus(TransactionStatus.SwapCompleted); - } - }, [ - approveError, - swapError, - isApprovalConfirming, - isSpendingApproved, - isSwapConfirming, - isSwapCompleted, - transactionStatus, - ]); - - useEffect(() => { - if (!transactionRequest || transactionStatus !== TransactionStatus.RouteRequested) return; - - console.log('Calling function to approve spending'); - // Approve the transactionRequest.target to spend fromAmount of fromToken - approveSpending().catch((error) => console.error('Error approving spending:', error)); - }, [approveSpending, transactionRequest, transactionStatus]); - - useEffect(() => { - if (!isSpendingApproved || transactionStatus !== TransactionStatus.SpendingApproved) return; - - console.log('Transaction approved, executing swap'); - // Execute the swap transaction - sendSwapTransaction().catch((error) => console.error('Error sending swap transaction:', error)); - }, [isSpendingApproved, sendSwapTransaction, transactionStatus]); - - useEffect(() => { - if (!hash || !isSwapCompleted) return; - - console.log('Transaction confirmed!'); - // Show the transaction receipt with Axelarscan link - const axelarScanLink = 'https://axelarscan.io/gmp/' + hash; - console.log(`Finished! Check Axelarscan for details: ${axelarScanLink}`); - - // Update transaction status until it completes - // We don't do anything with the follow-up for now, but we might in the future - // updateTransactionStatus(hash, requestId).catch((error) => - // console.error('Error updating transaction status:', error), - // ); - }, [hash, isSwapCompleted]); - - const executeSquidRouterSwap = useCallback(async () => { - if (!accountData.address || !amount) { - console.error('No account address found or amount found'); - return; - } - - // Reset the transaction status - setTransactionStatus(TransactionStatus.RouteRequested); - - // Start by getting the transaction request for the Route - getRouteTransactionRequest(accountData.address, amount) - .then(({ requestId, transactionRequest }) => { - setRequestId(requestId); - setTransactionRequest(transactionRequest); - }) - .catch((error) => console.error('Error sending transaction request:', error)); - }, [accountData.address, amount]); - - return { - transactionStatus, - executeSquidRouterSwap, - error: approveError || swapError, - }; -} diff --git a/src/services/squidrouter/process.ts b/src/services/squidrouter/process.ts new file mode 100644 index 00000000..4162973b --- /dev/null +++ b/src/services/squidrouter/process.ts @@ -0,0 +1,60 @@ +import { writeContract, sendTransaction, getAccount } from '@wagmi/core'; + +import { ExecutionContext, OfframpingState } from '../offrampingFlow'; +import erc20ABI from '../../contracts/ERC20'; +import { INPUT_TOKEN_CONFIG } from '../../constants/tokenConfig'; +import { getRouteTransactionRequest } from './route'; +import { waitForEvmTransaction } from '../evmTransactions'; +import { Keyring } from '@polkadot/api'; +import { getApiManagerInstance } from '../polkadot/polkadotApi'; + +export async function squidRouter(state: OfframpingState, { wagmiConfig }: ExecutionContext): Promise { + const inputToken = INPUT_TOKEN_CONFIG[state.inputTokenType]; + const fromTokenErc20Address = inputToken.erc20AddressSourceChain; + + const accountData = getAccount(wagmiConfig); + if (accountData?.address === undefined) { + throw new Error('Wallet not connected'); + } + + const pendulumApiComponents = await getApiManagerInstance(); + const apiData = pendulumApiComponents.apiData!; + + const keyring = new Keyring({ type: 'sr25519', ss58Format: apiData.ss58Format }); + const ephemeralKeypair = keyring.addFromUri(state.pendulumEphemeralSeed); + + const { transactionRequest } = await getRouteTransactionRequest( + accountData.address, + state.inputAmount.raw, + ephemeralKeypair.address, + inputToken, + ); + + console.log('Asking for approval of', transactionRequest?.target, fromTokenErc20Address, state.inputAmount.units); + + const approvalHash = await writeContract(wagmiConfig, { + abi: erc20ABI, + address: fromTokenErc20Address, + functionName: 'approve', + args: [transactionRequest?.target, state.inputAmount.raw], + }); + + await waitForEvmTransaction(approvalHash, wagmiConfig); + + const swapHash = await sendTransaction(wagmiConfig, { + to: transactionRequest.target, + data: transactionRequest.data, + value: transactionRequest.value, + gas: BigInt(transactionRequest.gasLimit) * BigInt(2), + }); + + const axelarScanLink = 'https://axelarscan.io/gmp/' + swapHash; + console.log(`Squidrouter Swap Initiated! Check Axelarscan for details: ${axelarScanLink}`); + + return { + ...state, + squidRouterApproveHash: approvalHash, + squidRouterSwapHash: swapHash, + phase: 'pendulumFundEphemeral', + }; +} diff --git a/src/services/squidrouter/route.ts b/src/services/squidrouter/route.ts index de74f7f4..3d91c0c0 100644 --- a/src/services/squidrouter/route.ts +++ b/src/services/squidrouter/route.ts @@ -4,14 +4,15 @@ import { squidReceiverABI } from '../../contracts/SquidReceiver'; import erc20ABI from '../../contracts/ERC20'; import { getSquidRouterConfig } from './config'; import encodePayload from './payload'; -import { getEphemeralAccount } from '../polkadot/ephemeral'; import { u8aToHex } from '@polkadot/util'; import { decodeAddress } from '@polkadot/util-crypto'; +import { InputTokenDetails } from '../../constants/tokenConfig'; interface RouteParams { fromAddress: string; fromChain: string; fromToken: string; + inputToken: InputTokenDetails; fromAmount: string; toChain: string; toToken: string; @@ -29,8 +30,14 @@ interface RouteParams { }; } -function createRouteParams(userAddress: string, amount: string): RouteParams { - const { fromToken, fromChainId, toChainId, receivingContractAddress, axlUSDC_MOONBEAM } = getSquidRouterConfig(); +function createRouteParams( + userAddress: string, + amount: string, + ephemeralAccountAddress: string, + inputToken: InputTokenDetails, +): RouteParams { + const { fromToken, fromChainId, toChainId, receivingContractAddress, axlUSDC_MOONBEAM } = + getSquidRouterConfig(inputToken); // TODO this must be approval, should we use max amount?? Or is this unsafe. const approvalErc20 = encodeFunctionData({ @@ -39,8 +46,7 @@ function createRouteParams(userAddress: string, amount: string): RouteParams { args: [receivingContractAddress, 1000000000], }); - const ephemeralAccount = getEphemeralAccount(); - const ephemeralAccountHex = u8aToHex(decodeAddress(ephemeralAccount.address)); + const ephemeralAccountHex = u8aToHex(decodeAddress(ephemeralAccountAddress)); const payload = encodePayload(ephemeralAccountHex); @@ -55,6 +61,7 @@ function createRouteParams(userAddress: string, amount: string): RouteParams { fromChain: fromChainId, fromToken: fromToken, fromAmount: amount, + inputToken, toChain: toChainId, toToken: axlUSDC_MOONBEAM, toAddress: userAddress, @@ -99,35 +106,9 @@ function createRouteParams(userAddress: string, amount: string): RouteParams { }; } -async function getRoute(params: RouteParams) { - const { integratorId } = getSquidRouterConfig(); - - try { - const result = await axios.post( - 'https://apiplus.squidrouter.com/v2/route', - - params, - { - headers: { - 'x-integrator-id': integratorId, - 'Content-Type': 'application/json', - }, - }, - ); - const requestId = result.headers['x-request-id']; // Retrieve request ID from response headers - return { data: result.data, requestId: requestId }; - } catch (error) { - if (error) { - console.error('API error:', (error as any).response.data); - } - console.error('Error with parameters:', params); - throw error; - } -} - async function getRouteApiPlus(params: RouteParams) { // This is the integrator ID for the Squid API by https://v2.app.squidrouter.com/ - const { integratorId } = getSquidRouterConfig(); + const { integratorId } = getSquidRouterConfig(params.inputToken); const url = 'https://apiplus.squidrouter.com/v2/route'; try { @@ -149,8 +130,13 @@ async function getRouteApiPlus(params: RouteParams) { } } -export async function getRouteTransactionRequest(userAddress: string, amount: string) { - const routeParams = createRouteParams(userAddress, amount); +export async function getRouteTransactionRequest( + userAddress: string, + amount: string, + ephemeralAccountAddress: string, + inputToken: InputTokenDetails, +) { + const routeParams = createRouteParams(userAddress, amount, ephemeralAccountAddress, inputToken); // Get the swap route using Squid API const routeResult = await getRouteApiPlus(routeParams); @@ -175,70 +161,3 @@ interface StatusParams { fromChainId: string; toChainId: string; } - -// Function to get the status of the transaction using Squid API -async function getStatus(params: StatusParams) { - const { integratorId } = getSquidRouterConfig(); - - try { - const result = await axios.get('https://v2.api.squidrouter.com/v2/status', { - params: { - transactionId: params.transactionId, - requestId: params.requestId, - fromChainId: params.fromChainId, - toChainId: params.toChainId, - }, - headers: { - 'x-integrator-id': integratorId, - }, - }); - return result.data; - } catch (error) { - if (error) { - console.error('API error:', (error as any).response.data); - } - console.error('Error with parameters:', params); - throw error; - } -} - -// Function to periodically check the transaction status until it completes -export async function updateTransactionStatus(txHash: string, requestId: string) { - const { fromChainId, toChainId } = getSquidRouterConfig(); - - const getStatusParams = { - transactionId: txHash, - requestId: requestId, - fromChainId: fromChainId, - toChainId: toChainId, - }; - - let status; - const completedStatuses = ['success', 'partial_success', 'needs_gas', 'not_found']; - const maxRetries = 15; // Maximum number of retries for status check - let retryCount = 0; - - do { - try { - status = await getStatus(getStatusParams); - console.log(`Route status: ${status.squidTransactionStatus}`); - } catch (error) { - if ((error as any).response && (error as any).response.status === 404) { - retryCount++; - if (retryCount >= maxRetries) { - console.error('Max retries reached. Transaction not found.'); - break; - } - console.log('Transaction not found. Retrying...'); - await new Promise((resolve) => setTimeout(resolve, 20000)); - continue; - } else { - throw error; - } - } - - if (!completedStatuses.includes(status.squidTransactionStatus)) { - await new Promise((resolve) => setTimeout(resolve, 5000)); - } - } while (!completedStatuses.includes(status.squidTransactionStatus)); -} diff --git a/src/services/stellar/index.tsx b/src/services/stellar/index.tsx index d4985fa2..179eb248 100644 --- a/src/services/stellar/index.tsx +++ b/src/services/stellar/index.tsx @@ -10,14 +10,16 @@ import { Account, } from 'stellar-sdk'; import { HORIZON_URL, BASE_FEE } from '../../constants/constants'; -import { Sep24Result } from '../anchor'; +import { SepResult } from '../anchor'; import { SIGNING_SERVICE_URL } from '../../constants/constants'; -import { TokenDetails } from '../../constants/tokenConfig'; +import { OUTPUT_TOKEN_CONFIG, OutputTokenDetails, OutputTokenType } from '../../constants/tokenConfig'; import { Buffer } from 'buffer'; const horizonServer = new Horizon.Server(HORIZON_URL); const NETWORK_PASSPHRASE = Networks.PUBLIC; import { EventStatus } from '../../components/GenericEvent'; +import { ExecutionContext, OfframpingState } from '../offrampingFlow'; +import { fetchSigningServiceAccountId } from '../signingService'; export interface StellarOperations { offrampingTransaction: Transaction; @@ -30,34 +32,62 @@ type StellarFundingSignatureResponse = { sequence: string; }; +export async function stellarCreateEphemeral( + stellarEphemeralSecret: string, + outputTokenType: OutputTokenType, +): Promise { + const fundingAccountId = await fetchSigningServiceAccountId(); + const ephemeralAccountExists = await isEphemeralCreated(stellarEphemeralSecret); + + if (!ephemeralAccountExists) { + await setupStellarAccount(fundingAccountId, stellarEphemeralSecret, outputTokenType); + + while (true) { + if (await isEphemeralCreated(stellarEphemeralSecret)) { + break; + } + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + } +} + +async function isEphemeralCreated(stellarEphemeralSecret: string): Promise { + const ephemeralKeypair = Keypair.fromSecret(stellarEphemeralSecret); + const ephemeralAccountId = ephemeralKeypair.publicKey(); + + try { + await horizonServer.loadAccount(ephemeralAccountId); + return true; + } catch { + return false; + } +} + export async function setUpAccountAndOperations( - fundingAccountPk: string, - sep24Result: Sep24Result, - ephemeralKeys: Keypair, - tokenConfig: TokenDetails, - renderEvent: (event: string, status: EventStatus) => void, + fundingAccountId: string, + ephemeralKeypair: Keypair, + sepResult: SepResult, + outputTokenType: OutputTokenType, ): Promise { - await setupStellarAccount(fundingAccountPk, ephemeralKeys, tokenConfig, renderEvent); - - const ephemeralAccountId = ephemeralKeys.publicKey(); - const ephemeralAccount = await horizonServer.loadAccount(ephemeralAccountId); + const ephemeralAccount = await horizonServer.loadAccount(ephemeralKeypair.publicKey()); const { offrampingTransaction, mergeAccountTransaction } = await createOfframpAndMergeTransaction( - fundingAccountPk, - sep24Result, - ephemeralKeys, + fundingAccountId, + sepResult, + ephemeralKeypair, ephemeralAccount, - tokenConfig, + OUTPUT_TOKEN_CONFIG[outputTokenType], ); return { offrampingTransaction, mergeAccountTransaction }; } async function setupStellarAccount( - fundingAccountPk: string, - ephemeralKeys: Keypair, - tokenConfig: TokenDetails, - renderEvent: (event: string, status: EventStatus) => void, + fundingAccountId: string, + ephemeralSecret: string, + outputTokenType: OutputTokenType, ) { - const ephemeralAccountId = ephemeralKeys.publicKey(); + const ephemeralKeypair = Keypair.fromSecret(ephemeralSecret); + const outputToken = OUTPUT_TOKEN_CONFIG[outputTokenType]; + const ephemeralAccountId = ephemeralKeypair.publicKey(); // To make the transaction deterministic, we need to set absoulte timebounds // We set the max time to 10 minutes from now @@ -68,7 +98,7 @@ async function setupStellarAccount( headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ accountId: ephemeralAccountId, maxTime, assetCode: tokenConfig.assetCode }), + body: JSON.stringify({ accountId: ephemeralAccountId, maxTime, assetCode: outputToken.stellarAsset.code.string }), }); if (!response.ok) { @@ -79,7 +109,7 @@ async function setupStellarAccount( // The funding account with sequene as per received from the server // This will be valid as long as teh funding account does not make // a transaction in the meantime - const fundingAccount = new Account(fundingAccountPk, responseData.sequence); + const fundingAccount = new Account(fundingAccountId, responseData.sequence); // add a setOption oeration in order to make this a 2-of-2 multisig account where the // funding account is a cosigner @@ -98,7 +128,7 @@ async function setupStellarAccount( .addOperation( Operation.setOptions({ source: ephemeralAccountId, - signer: { ed25519PublicKey: fundingAccountPk, weight: 1 }, + signer: { ed25519PublicKey: fundingAccountId, weight: 1 }, lowThreshold: 2, medThreshold: 2, highThreshold: 2, @@ -107,50 +137,40 @@ async function setupStellarAccount( .addOperation( Operation.changeTrust({ source: ephemeralAccountId, - asset: new Asset(tokenConfig.assetCode, tokenConfig.assetIssuer), + asset: new Asset(outputToken.stellarAsset.code.string, outputToken.stellarAsset.issuer.stellarEncoding), }), ) .setTimebounds(0, maxTime) .build(); } catch (error) { console.error(error); - renderEvent(`Could not create the account creation transaction. ${error}`, EventStatus.Error); throw new Error('Could not create the account creation transaction'); } - createAccountTransaction.addSignature(fundingAccountPk, responseData.signature[0]); - createAccountTransaction.sign(ephemeralKeys); + createAccountTransaction.addSignature(fundingAccountId, responseData.signature[0]); + createAccountTransaction.sign(ephemeralKeypair); try { await horizonServer.submitTransaction(createAccountTransaction); } catch (error: unknown) { const horizonError = error as { response: { data: { extras: any } } }; + console.log(horizonError.response.data.extras); console.error(horizonError.response.data.extras.toString()); - renderEvent( - `Could not submit the account creation transaction. ${JSON.stringify( - horizonError.response.data.extras.result_codes, - )}`, - EventStatus.Error, - ); throw new Error('Could not submit the account creation transaction'); } - - const ephemeralAccount = await horizonServer.loadAccount(ephemeralAccountId); - - return ephemeralAccount; } async function createOfframpAndMergeTransaction( - fundingAccountPk: string, - sep24Result: Sep24Result, + fundingAccountId: string, + sepResult: SepResult, ephemeralKeys: Keypair, ephemeralAccount: Account, - tokenConfig: TokenDetails, + { stellarAsset: { code, issuer } }: OutputTokenDetails, ) { // We allow for more TTL since the redeem may take time const maxTime = Date.now() + 1000 * 60 * 30; const sequence = ephemeralAccount.sequenceNumber(); - const { amount, memo, memoType, offrampingAccount } = sep24Result; + const { amount, memo, memoType, offrampingAccount } = sepResult; //cast the memo to corresponding type let transactionMemo; @@ -167,6 +187,8 @@ async function createOfframpAndMergeTransaction( throw new Error(`Unexpected offramp memo type: ${memoType}`); } + const stellarAsset = new Asset(code.string, issuer.stellarEncoding); + // this operation would run completely in the browser // that is where the signature of the ephemeral account is added const offrampingTransaction = new TransactionBuilder(ephemeralAccount, { @@ -176,7 +198,7 @@ async function createOfframpAndMergeTransaction( .addOperation( Operation.payment({ amount, - asset: new Asset(tokenConfig.assetCode, tokenConfig.assetIssuer), + asset: stellarAsset, destination: offrampingAccount, }), ) @@ -192,13 +214,13 @@ async function createOfframpAndMergeTransaction( }) .addOperation( Operation.changeTrust({ - asset: new Asset(tokenConfig.assetCode, tokenConfig.assetIssuer), + asset: stellarAsset, limit: '0', }), ) .addOperation( Operation.accountMerge({ - destination: fundingAccountPk, + destination: fundingAccountId, }), ) .setTimebounds(0, maxTime) @@ -217,10 +239,10 @@ async function createOfframpAndMergeTransaction( }, body: JSON.stringify({ accountId: ephemeralAccount.accountId(), - paymentData: sep24Result, + paymentData: sepResult, sequence, maxTime, - assetCode: tokenConfig.assetCode, + assetCode: code.string, }), }); @@ -240,29 +262,37 @@ async function createOfframpAndMergeTransaction( return { offrampingTransaction, mergeAccountTransaction }; } -export async function submitOfframpTransaction( - offrampingTransaction: Transaction, - renderEvent: (event: string, status: EventStatus) => void, -) { +// Recovery behaviour: If the offramp transaction was already submitted, we will get a sequence error. +// if we are on recovery mode we can ignore this error. +// Alternative improvement: check the balance of the destination (offramp) account to see if the funds arrived. +export async function stellarOfframp(state: OfframpingState): Promise { try { + const offrampingTransaction = new Transaction(state.stellarOfframpingTransaction, NETWORK_PASSPHRASE); await horizonServer.submitTransaction(offrampingTransaction); } catch (error) { const horizonError = error as { response: { data: { extras: any } } }; - renderEvent( + + console.log( `Could not submit the offramp transaction ${JSON.stringify(horizonError.response.data.extras.result_codes)}`, - EventStatus.Error, ); - - console.error(horizonError.response.data.extras); - throw new Error('Could not submit the offramping transaction'); + // check https://developers.stellar.org/docs/data/horizon/api-reference/errors/result-codes/transactions + if (horizonError.response.data.extras.result_codes.transaction === 'tx_bad_seq') { + console.log('Recovery mode: Offramp already performed.'); + } else { + console.error(horizonError.response.data.extras); + throw new Error('Could not submit the offramping transaction'); + } } + + return { ...state, phase: 'stellarCleanup' }; } -export async function cleanupStellarEphemeral( - mergeAccountTransaction: Transaction, - renderEvent: (event: string, status: EventStatus) => void, -) { +export async function stellarCleanup( + state: OfframpingState, + { renderEvent }: ExecutionContext, +): Promise { try { + const mergeAccountTransaction = new Transaction(state.stellarCleanupTransaction, NETWORK_PASSPHRASE); await horizonServer.submitTransaction(mergeAccountTransaction); } catch (error) { const horizonError = error as { response: { data: { extras: any } } }; @@ -275,4 +305,6 @@ export async function cleanupStellarEphemeral( console.error(horizonError.response.data.extras); throw new Error('Could not submit the cleanup transaction'); } + + return undefined; } diff --git a/src/services/stellar/utils.tsx b/src/services/stellar/utils.tsx index 9b306e8d..e7f19c67 100644 --- a/src/services/stellar/utils.tsx +++ b/src/services/stellar/utils.tsx @@ -1,16 +1,21 @@ import { Horizon, Keypair } from 'stellar-sdk'; import { HORIZON_URL } from '../../constants/constants'; +import Big from 'big.js'; -export const checkStellarAccount = async (secret: string): Promise => { +export const getStellarBalanceUnits = async (publicKey: string, assetCode: string): Promise => { try { const server = new Horizon.Server(HORIZON_URL); - const keypair = Keypair.fromSecret(secret); + const account = await server.loadAccount(publicKey); + let balanceUnits = '0'; + account.balances.forEach((balance) => { + if (balance.asset_type === 'credit_alphanum4' && balance.asset_code === assetCode) { + balanceUnits = balance.balance; + } + }); - //loading the account should be enough check if the account exists - const account = await server.loadAccount(keypair.publicKey()); - return true; + return new Big(balanceUnits); } catch (error) { - console.error('Stellar Account Error:', error); - return false; + console.log(error); + throw new Error('Error Reading Stellar Balance'); } }; diff --git a/src/services/storage/local.ts b/src/services/storage/local.ts index 5dcb5eb1..3b0dfd47 100644 --- a/src/services/storage/local.ts +++ b/src/services/storage/local.ts @@ -1,6 +1,7 @@ import { Storage } from './types'; const exists = (value?: string | null): value is string => !!value && value.length > 0; + export const storageService: Storage = { get: (key, defaultValue?) => { if (!localStorage) return defaultValue; diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 00000000..833b0063 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,30 @@ +import { InputTokenType, OutputTokenType } from '../constants/tokenConfig'; + +export interface SwapOptions { + assetIn: string; + minAmountOut?: Big; +} + +export enum OperationStatus { + Idle, + Sep10Completed, + SepCompleted, // Sep6 or Sep24 completed. Ready to transfer funds. + BridgeExecuted, // Confirmation that the bridge (squid for now) transaction went through + PendulumEphemeralReady, // Confirmation that the ephemeral received both the expected tokens and the native balance + NablaSwapApproved, // Confirmation that the tokens where approved + NablaSwapPerformed, // Confirmation that the swap went through + StellarEphemeralFunded, // Ephemeral account was created and funded + StellarEphemeralReady, // Operations for transfer and cleaning were created and saved + Redeemed, // Confirmation that the redeem tx went through + Offramped, // Confirmation that stellar transaction to offramp went through + StellarCleaned, // Confirmation that the stellar account was merged + Error, +} + +export interface ExecutionInput { + inputTokenType: InputTokenType; + outputTokenType: OutputTokenType; + amountInUnits: string; + nablaAmountInRaw: string; + minAmountOutUnits: string; +} diff --git a/yarn.lock b/yarn.lock index a672e69c..3328c817 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1487,26 +1487,6 @@ __metadata: languageName: node linkType: hard -"@chainsafe/metamask-polkadot-adapter@npm:^0.6.0": - version: 0.6.0 - resolution: "@chainsafe/metamask-polkadot-adapter@npm:0.6.0" - dependencies: - "@polkadot/api": "npm:^10.9.1" - "@polkadot/extension-inject": "npm:^0.46.5" - "@polkadot/types-augment": "npm:^10.9.1" - checksum: 32635c3a6ead8adfa7b8f99bbfa6548bc7f0632f13bb1c6f2e7833e6d1e0761df1944fcfcf10c3648a5e2dde3cd00a0f3e355ca68e1626eca817dc3e3d6a941e - languageName: node - linkType: hard - -"@chainsafe/metamask-polkadot-types@npm:^0.7.0": - version: 0.7.0 - resolution: "@chainsafe/metamask-polkadot-types@npm:0.7.0" - dependencies: - "@polkadot/api": "npm:^10.9.1" - checksum: 50078a332d1e4776b7eaacc193180d6c715f2b35c6e49f3e9bf526018383b1d67044839e3f6d074ca27b3081593cc98a01546f0676d535f2c378d5a1344296f1 - languageName: node - linkType: hard - "@coinbase/wallet-sdk@npm:4.0.3": version: 4.0.3 resolution: "@coinbase/wallet-sdk@npm:4.0.3" @@ -3407,7 +3387,7 @@ __metadata: languageName: node linkType: hard -"@polkadot/api@npm:10.13.1, @polkadot/api@npm:^10.12.4, @polkadot/api@npm:^10.13.1, @polkadot/api@npm:^10.9.1": +"@polkadot/api@npm:10.13.1, @polkadot/api@npm:^10.13.1": version: 10.13.1 resolution: "@polkadot/api@npm:10.13.1" dependencies: @@ -3532,24 +3512,6 @@ __metadata: languageName: node linkType: hard -"@polkadot/extension-inject@npm:^0.46.5": - version: 0.46.9 - resolution: "@polkadot/extension-inject@npm:0.46.9" - dependencies: - "@polkadot/api": "npm:^10.12.4" - "@polkadot/rpc-provider": "npm:^10.12.4" - "@polkadot/types": "npm:^10.12.4" - "@polkadot/util": "npm:^12.6.2" - "@polkadot/util-crypto": "npm:^12.6.2" - "@polkadot/x-global": "npm:^12.6.2" - tslib: "npm:^2.6.2" - peerDependencies: - "@polkadot/api": "*" - "@polkadot/util": "*" - checksum: f5750d9ce6a4350714185808e3b3404e76d073e7591d2716a514633933cc1e2a275a84a5e98bd8d765f1ce372e1ef5205c6306a56679fa46d9be175232e05cd4 - languageName: node - linkType: hard - "@polkadot/keyring@npm:^10.1.9, @polkadot/keyring@npm:^10.4.2": version: 10.4.2 resolution: "@polkadot/keyring@npm:10.4.2" @@ -3706,7 +3668,7 @@ __metadata: languageName: node linkType: hard -"@polkadot/rpc-provider@npm:10.13.1, @polkadot/rpc-provider@npm:^10.12.4, @polkadot/rpc-provider@npm:^10.13.1": +"@polkadot/rpc-provider@npm:10.13.1, @polkadot/rpc-provider@npm:^10.13.1": version: 10.13.1 resolution: "@polkadot/rpc-provider@npm:10.13.1" dependencies: @@ -3778,7 +3740,7 @@ __metadata: languageName: node linkType: hard -"@polkadot/types-augment@npm:10.13.1, @polkadot/types-augment@npm:^10.9.1": +"@polkadot/types-augment@npm:10.13.1": version: 10.13.1 resolution: "@polkadot/types-augment@npm:10.13.1" dependencies: @@ -3952,7 +3914,7 @@ __metadata: languageName: node linkType: hard -"@polkadot/types@npm:10.13.1, @polkadot/types@npm:^10.12.4, @polkadot/types@npm:^10.13.1": +"@polkadot/types@npm:10.13.1, @polkadot/types@npm:^10.13.1": version: 10.13.1 resolution: "@polkadot/types@npm:10.13.1" dependencies: @@ -5235,6 +5197,15 @@ __metadata: languageName: node linkType: hard +"@testing-library/user-event@npm:^14.5.2": + version: 14.5.2 + resolution: "@testing-library/user-event@npm:14.5.2" + peerDependencies: + "@testing-library/dom": ">=7.21.4" + checksum: 49821459d81c6bc435d97128d6386ca24f1e4b3ba8e46cb5a96fe3643efa6e002d88c1b02b7f2ec58da593e805c59b78d7fdf0db565c1f02ba782f63ee984040 + languageName: node + linkType: hard + "@tootallnate/once@npm:2": version: 2.0.0 resolution: "@tootallnate/once@npm:2.0.0" @@ -5520,7 +5491,7 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:*, @types/react@npm:^18.0.28": +"@types/react@npm:*": version: 18.2.5 resolution: "@types/react@npm:18.2.5" dependencies: @@ -10032,6 +10003,26 @@ __metadata: languageName: node linkType: hard +"framer-motion@npm:^11.2.14": + version: 11.2.14 + resolution: "framer-motion@npm:11.2.14" + dependencies: + tslib: "npm:^2.4.0" + peerDependencies: + "@emotion/is-prop-valid": "*" + react: ^18.0.0 + react-dom: ^18.0.0 + peerDependenciesMeta: + "@emotion/is-prop-valid": + optional: true + react: + optional: true + react-dom: + optional: true + checksum: 741ad03761780fe6103c02f39bc86addba6900a4f395f94b06361285adc7bcb1c6bc21eb88b791a1ce303b4ededb29c5c262e9d86e3afd27915dedc3673a0842 + languageName: node + linkType: hard + "fresh@npm:0.5.2": version: 0.5.2 resolution: "fresh@npm:0.5.2" @@ -13930,8 +13921,6 @@ __metadata: "@babel/plugin-proposal-class-properties": "npm:^7.18.6" "@babel/preset-env": "npm:^7.20.2" "@babel/preset-typescript": "npm:^7.18.6" - "@chainsafe/metamask-polkadot-adapter": "npm:^0.6.0" - "@chainsafe/metamask-polkadot-types": "npm:^0.7.0" "@emotion/react": "npm:^11.11.1" "@emotion/styled": "npm:^11.11.0" "@fontsource/roboto": "npm:^5.0.8" @@ -13967,11 +13956,11 @@ __metadata: "@testing-library/jest-dom": "npm:^5.16.5" "@testing-library/preact": "npm:^3.2.3" "@testing-library/preact-hooks": "npm:^1.1.0" + "@testing-library/user-event": "npm:^14.5.2" "@types/big.js": "npm:^6" "@types/bn.js": "npm:^5" "@types/jest": "npm:^29.4.0" "@types/node": "npm:^18.14.1" - "@types/react": "npm:^18.0.28" "@types/testing-library__jest-dom": "npm:^5.14.5" "@typescript-eslint/eslint-plugin": "npm:^5.53.0" "@typescript-eslint/parser": "npm:^5.53.0" @@ -13988,6 +13977,7 @@ __metadata: eslint-plugin-jest: "npm:^27.2.1" eslint-plugin-react: "npm:^7.32.2" eslint-plugin-react-hooks: "npm:^4.6.0" + framer-motion: "npm:^11.2.14" husky: "npm:>=6" jest: "npm:^29.4.3" jest-environment-jsdom: "npm:^29.4.3" @@ -16869,7 +16859,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.0": +"tslib@npm:^2.0.0, tslib@npm:^2.4.0": version: 2.6.3 resolution: "tslib@npm:2.6.3" checksum: 52109bb681f8133a2e58142f11a50e05476de4f075ca906d13b596ae5f7f12d30c482feb0bff167ae01cfc84c5803e575a307d47938999246f5a49d174fc558c