From f4eef4291da2a40ed88a094370737b4e1aeeca11 Mon Sep 17 00:00:00 2001 From: Robin <70903912+Robeartt@users.noreply.github.com> Date: Sun, 14 Jul 2024 18:30:24 +0200 Subject: [PATCH] New UI --- .env | 7 + .env.production | 7 + .env.testnet | 2 +- .vscode/extensions.json | 7 - .vscode/launch.json | 28 - .vscode/settings.json | 14 - next.config.mjs | 14 +- package-lock.json | 670 ++++++++++++++++-- package.json | 20 +- public/icons/Orbit_Logo.svg | 31 + .../backstop/BackstopDepositAnvil.tsx | 2 +- src/components/backstop/BackstopExitAnvil.tsx | 2 +- src/components/backstop/BackstopJoinAnvil.tsx | 2 +- .../backstop/BackstopQueueAnvil.tsx | 2 +- src/components/backstop/BackstopQueueMod.tsx | 3 +- src/components/borrow/BorrowAnvil.tsx | 110 ++- src/components/borrow/BorrowPositionCard.tsx | 2 +- src/components/common/AnvilAlert.tsx | 1 + src/components/common/CoinSelectMenu.tsx | 113 +++ src/components/common/Divider.tsx | 5 +- src/components/common/ReserveDropdown.tsx | 82 ++- src/components/common/SectionBase.tsx | 2 +- src/components/common/ToggleButton.tsx | 2 +- src/components/common/WalletWarning.tsx | 34 +- src/components/dashboard/PositionOverview.tsx | 1 - src/components/lend/LendAnvil.tsx | 412 ++++++++--- src/components/lend/LendPositionCard.tsx | 2 +- src/components/markets/MarketCard.tsx | 1 - src/components/nav/NavBar.tsx | 28 +- src/components/nav/NavMenu.tsx | 10 - src/components/nav/WalletMenu.tsx | 36 +- src/components/repay/RepayAnvil.tsx | 2 +- .../swap/SwapCurrencyInputPanel.tsx | 51 ++ src/components/swap/styled.tsx | 45 ++ src/components/withdraw/WithdrawAnvil.tsx | 2 +- src/contexts/wallet.tsx | 22 +- src/functions/getCurrentTimePlusOneHour.ts | 9 + src/helpers/aggregator/index.ts | 36 + src/helpers/convert.ts | 170 +++++ src/helpers/utils.tsx | 205 ++++++ src/helpers/xdr/bigint-encoder.ts | 77 ++ src/helpers/xdr/errors.ts | 23 + src/helpers/xdr/i128.ts | 17 + src/helpers/xdr/index.ts | 1 + src/helpers/xdr/large-int.ts | 116 +++ src/helpers/xdr/serialization/xdr-reader.ts | 86 +++ src/helpers/xdr/serialization/xdr-writer.ts | 105 +++ src/helpers/xdr/xdr-type.ts | 170 +++++ src/hooks/useRouterAddress.tsx | 25 + src/hooks/useRouterCallback.tsx | 74 ++ src/hooks/useSwapCallback.tsx | 111 +++ src/interfaces/adminKeys.ts | 10 + src/interfaces/currency.ts | 6 + src/interfaces/index.ts | 3 + src/interfaces/pairs.ts | 18 + src/interfaces/tokens.ts | 23 + src/layouts/DefaultLayout.tsx | 13 +- src/pages/borrow.tsx | 84 +-- src/pages/dashboard.tsx | 50 -- src/pages/index.tsx | 140 +++- src/pages/swap.tsx | 256 +++++++ src/pages/termsofservice.tsx | 23 - src/services/axios.ts | 10 + src/services/router.ts | 10 + src/state/routing/types.ts | 301 ++++++++ src/store/blendSlice.ts | 4 +- src/store/userSlice.ts | 2 +- src/theme.ts | 20 +- tsconfig.json | 3 +- 69 files changed, 3529 insertions(+), 446 deletions(-) create mode 100644 .env create mode 100644 .env.production delete mode 100644 .vscode/extensions.json delete mode 100644 .vscode/launch.json delete mode 100644 .vscode/settings.json create mode 100644 public/icons/Orbit_Logo.svg create mode 100644 src/components/common/CoinSelectMenu.tsx create mode 100644 src/components/swap/SwapCurrencyInputPanel.tsx create mode 100644 src/components/swap/styled.tsx create mode 100644 src/functions/getCurrentTimePlusOneHour.ts create mode 100644 src/helpers/aggregator/index.ts create mode 100644 src/helpers/convert.ts create mode 100644 src/helpers/utils.tsx create mode 100644 src/helpers/xdr/bigint-encoder.ts create mode 100644 src/helpers/xdr/errors.ts create mode 100644 src/helpers/xdr/i128.ts create mode 100644 src/helpers/xdr/index.ts create mode 100644 src/helpers/xdr/large-int.ts create mode 100644 src/helpers/xdr/serialization/xdr-reader.ts create mode 100644 src/helpers/xdr/serialization/xdr-writer.ts create mode 100644 src/helpers/xdr/xdr-type.ts create mode 100644 src/hooks/useRouterAddress.tsx create mode 100644 src/hooks/useRouterCallback.tsx create mode 100644 src/hooks/useSwapCallback.tsx create mode 100644 src/interfaces/adminKeys.ts create mode 100644 src/interfaces/currency.ts create mode 100644 src/interfaces/index.ts create mode 100644 src/interfaces/pairs.ts create mode 100644 src/interfaces/tokens.ts create mode 100644 src/pages/swap.tsx delete mode 100644 src/pages/termsofservice.tsx create mode 100644 src/services/axios.ts create mode 100644 src/services/router.ts create mode 100644 src/state/routing/types.ts diff --git a/.env b/.env new file mode 100644 index 0000000..69c022c --- /dev/null +++ b/.env @@ -0,0 +1,7 @@ +NEXT_PUBLIC_STELLAR_EXPERT_URL=https://stellar.expert/explorer/testnet +NEXT_PUBLIC_RPC_URL=https://soroban-testnet.stellar.org +NEXT_PUBLIC_HORIZON_URL=https://horizon-testnet.stellar.org +NEXT_PUBLIC_PASSPHRASE=Test SDF Network ; September 2015 +NEXT_PUBLIC_BACKSTOP=CBNRD2GEJP4Y3H3TG3TDNJFGOVBIBBL6SDQ5LUPT4LUQP62T3LLRGHPK +NEXT_PUBLIC_USDC_ISSUER=GATALTGTWIOT6BUDBCZM3Q4OQ4BO2COLOAZ7IYSKPLC2PMSOPPGF5V56 +NEXT_PUBLIC_BLND_ISSUER=GATALTGTWIOT6BUDBCZM3Q4OQ4BO2COLOAZ7IYSKPLC2PMSOPPGF5V56 \ No newline at end of file diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..ccfabd4 --- /dev/null +++ b/.env.production @@ -0,0 +1,7 @@ +NEXT_PUBLIC_STELLAR_EXPERT_URL=https://stellar.expert/explorer/public +NEXT_PUBLIC_RPC_URL=https://soroban-rpc.creit.tech/ +NEXT_PUBLIC_HORIZON_URL=https://horizon.stellar.org +NEXT_PUBLIC_PASSPHRASE=Public Global Stellar Network ; September 2015 +NEXT_PUBLIC_BACKSTOP=CAO3AGAMZVRMHITL36EJ2VZQWKYRPWMQAPDQD5YEOF3GIF7T44U4JAL3 +NEXT_PUBLIC_USDC_ISSUER=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN +NEXT_PUBLIC_BLND_ISSUER=GDJEHTBE6ZHUXSWFI642DCGLUOECLHPF3KSXHPXTSTJ7E3JF6MQ5EZYY \ No newline at end of file diff --git a/.env.testnet b/.env.testnet index 02f0bcb..69c022c 100644 --- a/.env.testnet +++ b/.env.testnet @@ -4,4 +4,4 @@ NEXT_PUBLIC_HORIZON_URL=https://horizon-testnet.stellar.org NEXT_PUBLIC_PASSPHRASE=Test SDF Network ; September 2015 NEXT_PUBLIC_BACKSTOP=CBNRD2GEJP4Y3H3TG3TDNJFGOVBIBBL6SDQ5LUPT4LUQP62T3LLRGHPK NEXT_PUBLIC_USDC_ISSUER=GATALTGTWIOT6BUDBCZM3Q4OQ4BO2COLOAZ7IYSKPLC2PMSOPPGF5V56 -NEXT_PUBLIC_BLND_ISSUER=GBUYQYM4HJ6PRTLB77W2HNMY37KJGP5PLY367V7SYCO24NMA6TGWQOSW \ No newline at end of file +NEXT_PUBLIC_BLND_ISSUER=GATALTGTWIOT6BUDBCZM3Q4OQ4BO2COLOAZ7IYSKPLC2PMSOPPGF5V56 \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index daaa5ee..0000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "recommendations": [ - "arcanis.vscode-zipfs", - "dbaeumer.vscode-eslint", - "esbenp.prettier-vscode" - ] -} diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index cbb9f4a..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Next.js: debug server-side", - "type": "node-terminal", - "request": "launch", - "command": "npm run dev" - }, - { - "name": "Next.js: debug client-side", - "type": "chrome", - "request": "launch", - "url": "http://localhost:3000" - }, - { - "name": "Next.js: debug full stack", - "type": "node-terminal", - "request": "launch", - "command": "npm run dev", - "serverReadyAction": { - "pattern": "started server on .+, url: (https?://.+)", - "uriFormat": "%s", - "action": "debugWithChrome" - } - } - ] -} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index a60b790..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "search.exclude": { - "**/.yarn": true, - "**/.pnp.*": true - }, - "typescript.enablePromptUseWorkspaceTsdk": true, - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true, - "editor.tabSize": 2, - "editor.codeActionsOnSave": { - "source.fixAll": "explicit", - "source.organizeImports": "explicit" - } -} diff --git a/next.config.mjs b/next.config.mjs index da19137..02f593b 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -7,6 +7,18 @@ const nextConfig = { unoptimized: true, // Note: Required for static builds }, trailingSlash: true, + env: { + BASE_URL: process.env.BASE_URL, + }, + async redirects() { + return [ + { + source: '/', + destination: '/borrow', + permanent: true, + }, + ]; + }, }; -export default nextConfig +export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 3fa1c64..562129e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,27 +1,33 @@ { "name": "blend-ui", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "blend-ui", - "version": "1.0.0", + "version": "1.0.1", "dependencies": { - "@blend-capital/blend-sdk": "^1.1.0", - "@creit.tech/stellar-wallets-kit": "0.8.1", + "@blend-capital/blend-sdk": "1.2.0", + "@creit.tech/stellar-wallets-kit": "0.8.2", "@emotion/react": "^11.9.3", "@emotion/styled": "^11.9.3", "@mui/icons-material": "^5.8.4", "@mui/material": "^5.8.6", + "@soroban-react/contracts": "^9.1.10", + "@soroban-react/core": "^9.1.10", "@stellar/freighter-api": "^2.0.0", - "@stellar/stellar-sdk": "11.3.0", + "@stellar/stellar-sdk": "^12.1.0", + "axios": "^1.7.2", + "bigint-conversion": "^2.4.3", "bignumber.js": "^9.0.2", "copy-to-clipboard": "^3.3.3", "next": "^12.3.4", "react": "^18.2.0", "react-countdown": "^2.3.5", "react-dom": "^18.2.0", + "soroswap-router-sdk": "^1.2.9", + "swr": "^2.2.5", "zustand": "^4.3.7" }, "devDependencies": { @@ -191,11 +197,11 @@ } }, "node_modules/@blend-capital/blend-sdk": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@blend-capital/blend-sdk/-/blend-sdk-1.1.0.tgz", - "integrity": "sha512-kHo7isdrGzdliO90Ip4uNUMu/07MaaP1c0wB+gESJPUMYkxJ/rzsfC/QkHE1xjisKFhiBIc75wjkBG0o+D5LTw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@blend-capital/blend-sdk/-/blend-sdk-1.2.0.tgz", + "integrity": "sha512-6DcJ043mikVH0HfegHHUsbiOt/KfGaw3CFpcHmOMClOb9O6Biwz+Gra0ugld+XfVo0eXjYbC8CPOaCD7jVty9Q==", "dependencies": { - "@stellar/stellar-sdk": "11.3.0", + "@stellar/stellar-sdk": "12.1.0", "buffer": "6.0.3", "follow-redirects": ">=1.15.6" } @@ -210,9 +216,9 @@ } }, "node_modules/@creit.tech/stellar-wallets-kit": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@creit.tech/stellar-wallets-kit/-/stellar-wallets-kit-0.8.1.tgz", - "integrity": "sha512-80k3GIHdxiyzTqLAC6YVM3FgBf30xHD5jdAcRqLRumoQXikQkkQs2cvAxK58GrQtBvighlWsNkMOzprkUQ7ryw==", + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@creit.tech/stellar-wallets-kit/-/stellar-wallets-kit-0.8.2.tgz", + "integrity": "sha512-Uqa7ywA/vj+8+LTuDbLoE12OhFfuL/y9lziTKHv6jmmUnN9oPr1e6qiSX1kMJ+y9HOrDcj/e65kSck5Ao0i1dQ==", "dependencies": { "@albedo-link/intent": "0.12.0", "@creit-tech/xbull-wallet-connect": "github:Creit-Tech/xBull-Wallet-Connect#0.2.0", @@ -437,6 +443,11 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@juanelas/base64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@juanelas/base64/-/base64-1.1.5.tgz", + "integrity": "sha512-mjAF27LzwfYobdwqnxZgeucbKT5wRRNvILg3h5OvCWK+3F7mw/A1tnjHnNiTYtLmTvT/bM1jA5AX7eQawDGs1w==" + }, "node_modules/@lit-labs/ssr-dom-shim": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.0.tgz", @@ -1321,6 +1332,245 @@ "integrity": "sha512-S3Kq8e7LqxkA9s7HKLqXGTGck1uwis5vAXan3FnU5yw1Ec5hsSGnq4s/UCaSqABPOnOTg7zASLyst7+ohgWexg==", "dev": true }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==" + }, + "node_modules/@soroban-react/contracts": { + "version": "9.1.10", + "resolved": "https://registry.npmjs.org/@soroban-react/contracts/-/contracts-9.1.10.tgz", + "integrity": "sha512-hneHC9cDiQ/MqpQ8OAz16RtEivZSdYg5rDDpjWr1QpNWSgGIcEA6J0FsLb1PYbMeKbFaskO7Rc6PD047TpiBfQ==", + "dependencies": { + "@soroban-react/core": "^9.1.10", + "@stellar/stellar-sdk": "11.3.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/@soroban-react/contracts/node_modules/@stellar/stellar-base": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-11.0.1.tgz", + "integrity": "sha512-VQh+1KEtFjegD6spx08+lENt8tQOkQQQZoLtqExjpRXyWlqDhEe+bXMlBTYKDc5MIynHyD42RPEib27UG17trA==", + "dependencies": { + "@stellar/js-xdr": "^3.1.1", + "base32.js": "^0.1.0", + "bignumber.js": "^9.1.2", + "buffer": "^6.0.3", + "sha.js": "^2.3.6", + "tweetnacl": "^1.0.3" + }, + "optionalDependencies": { + "sodium-native": "^4.0.10" + } + }, + "node_modules/@soroban-react/contracts/node_modules/@stellar/stellar-sdk": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-11.3.0.tgz", + "integrity": "sha512-i+heopibJNRA7iM8rEPz0AXphBPYvy2HDo8rxbDwWpozwCfw8kglP9cLkkhgJe8YicgLrdExz/iQZaLpqLC+6w==", + "dependencies": { + "@stellar/stellar-base": "^11.0.1", + "axios": "^1.6.8", + "bignumber.js": "^9.1.2", + "eventsource": "^2.0.2", + "randombytes": "^2.1.0", + "toml": "^3.0.0", + "urijs": "^1.19.1" + } + }, + "node_modules/@soroban-react/core": { + "version": "9.1.10", + "resolved": "https://registry.npmjs.org/@soroban-react/core/-/core-9.1.10.tgz", + "integrity": "sha512-6FpdFXtyOOxl27XsNZ9PccHsYJB9z3Vmx815pqJ/nUE0PMT78BuEOhox4F56Lkpb1/YijiQ8TuN6xWoBJ29hbg==", + "dependencies": { + "@soroban-react/freighter": "^9.1.10", + "@soroban-react/lobstr": "^9.1.10", + "@soroban-react/types": "^9.1.10", + "@soroban-react/xbull": "^1.0.1", + "@stellar/stellar-sdk": "11.3.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/@soroban-react/core/node_modules/@stellar/stellar-base": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-11.0.1.tgz", + "integrity": "sha512-VQh+1KEtFjegD6spx08+lENt8tQOkQQQZoLtqExjpRXyWlqDhEe+bXMlBTYKDc5MIynHyD42RPEib27UG17trA==", + "dependencies": { + "@stellar/js-xdr": "^3.1.1", + "base32.js": "^0.1.0", + "bignumber.js": "^9.1.2", + "buffer": "^6.0.3", + "sha.js": "^2.3.6", + "tweetnacl": "^1.0.3" + }, + "optionalDependencies": { + "sodium-native": "^4.0.10" + } + }, + "node_modules/@soroban-react/core/node_modules/@stellar/stellar-sdk": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-11.3.0.tgz", + "integrity": "sha512-i+heopibJNRA7iM8rEPz0AXphBPYvy2HDo8rxbDwWpozwCfw8kglP9cLkkhgJe8YicgLrdExz/iQZaLpqLC+6w==", + "dependencies": { + "@stellar/stellar-base": "^11.0.1", + "axios": "^1.6.8", + "bignumber.js": "^9.1.2", + "eventsource": "^2.0.2", + "randombytes": "^2.1.0", + "toml": "^3.0.0", + "urijs": "^1.19.1" + } + }, + "node_modules/@soroban-react/freighter": { + "version": "9.1.10", + "resolved": "https://registry.npmjs.org/@soroban-react/freighter/-/freighter-9.1.10.tgz", + "integrity": "sha512-fLIG3ZmCOMQg2Vd31xwv4E2Pzrjs573YBIK5Rrd9r0qoIeCBzVOxCpaYD7N1sJO53+VFxUGlYs6wtYM77TW76w==", + "dependencies": { + "@soroban-react/types": "^9.1.10", + "@stellar/freighter-api": "1.7.1" + } + }, + "node_modules/@soroban-react/freighter/node_modules/@stellar/freighter-api": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@stellar/freighter-api/-/freighter-api-1.7.1.tgz", + "integrity": "sha512-XvPO+XgEbkeP0VhP0U1edOkds+rGS28+y8GRGbCVXeZ9ZslbWqRFQoETAdX8IXGuykk2ib/aPokiLc5ZaWYP7w==" + }, + "node_modules/@soroban-react/lobstr": { + "version": "9.1.10", + "resolved": "https://registry.npmjs.org/@soroban-react/lobstr/-/lobstr-9.1.10.tgz", + "integrity": "sha512-1HP64JbNA5C+kbCud4c+dufX439EeNfkpxhscUOAm04/j8VGLUJJpZecwi05BBlEaOB7SEIUl6+OA55ubht0Nw==", + "dependencies": { + "@lobstrco/signer-extension-api": "^1.0.0-beta.0", + "@soroban-react/types": "^9.1.10", + "@stellar/stellar-sdk": "11.3.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/@soroban-react/lobstr/node_modules/@stellar/stellar-base": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-11.0.1.tgz", + "integrity": "sha512-VQh+1KEtFjegD6spx08+lENt8tQOkQQQZoLtqExjpRXyWlqDhEe+bXMlBTYKDc5MIynHyD42RPEib27UG17trA==", + "dependencies": { + "@stellar/js-xdr": "^3.1.1", + "base32.js": "^0.1.0", + "bignumber.js": "^9.1.2", + "buffer": "^6.0.3", + "sha.js": "^2.3.6", + "tweetnacl": "^1.0.3" + }, + "optionalDependencies": { + "sodium-native": "^4.0.10" + } + }, + "node_modules/@soroban-react/lobstr/node_modules/@stellar/stellar-sdk": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-11.3.0.tgz", + "integrity": "sha512-i+heopibJNRA7iM8rEPz0AXphBPYvy2HDo8rxbDwWpozwCfw8kglP9cLkkhgJe8YicgLrdExz/iQZaLpqLC+6w==", + "dependencies": { + "@stellar/stellar-base": "^11.0.1", + "axios": "^1.6.8", + "bignumber.js": "^9.1.2", + "eventsource": "^2.0.2", + "randombytes": "^2.1.0", + "toml": "^3.0.0", + "urijs": "^1.19.1" + } + }, + "node_modules/@soroban-react/types": { + "version": "9.1.10", + "resolved": "https://registry.npmjs.org/@soroban-react/types/-/types-9.1.10.tgz", + "integrity": "sha512-WHZ0HLZjC0+MIhtlhmrz/danPd3XE7LcRnwa1c2eyXX2tKaI+QIMBR2Qd7GYEWJ/9xJPPF2ThgGFbDqh5XXIsg==", + "dependencies": { + "@stellar/stellar-sdk": "11.3.0" + } + }, + "node_modules/@soroban-react/types/node_modules/@stellar/stellar-base": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-11.0.1.tgz", + "integrity": "sha512-VQh+1KEtFjegD6spx08+lENt8tQOkQQQZoLtqExjpRXyWlqDhEe+bXMlBTYKDc5MIynHyD42RPEib27UG17trA==", + "dependencies": { + "@stellar/js-xdr": "^3.1.1", + "base32.js": "^0.1.0", + "bignumber.js": "^9.1.2", + "buffer": "^6.0.3", + "sha.js": "^2.3.6", + "tweetnacl": "^1.0.3" + }, + "optionalDependencies": { + "sodium-native": "^4.0.10" + } + }, + "node_modules/@soroban-react/types/node_modules/@stellar/stellar-sdk": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-11.3.0.tgz", + "integrity": "sha512-i+heopibJNRA7iM8rEPz0AXphBPYvy2HDo8rxbDwWpozwCfw8kglP9cLkkhgJe8YicgLrdExz/iQZaLpqLC+6w==", + "dependencies": { + "@stellar/stellar-base": "^11.0.1", + "axios": "^1.6.8", + "bignumber.js": "^9.1.2", + "eventsource": "^2.0.2", + "randombytes": "^2.1.0", + "toml": "^3.0.0", + "urijs": "^1.19.1" + } + }, + "node_modules/@soroban-react/xbull": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@soroban-react/xbull/-/xbull-1.0.1.tgz", + "integrity": "sha512-dk/qoF6CsRMgKWyO++woJmq5bTgo9ZCGBtGaNFOiljliQ7bX6MzvMoeZSwTvNNJZhL18AWpaiWmxCvv2bJJoQg==", + "dependencies": { + "@creit-tech/xbull-wallet-connect": "github:Creit-Tech/xBull-Wallet-Connect", + "@soroban-react/types": "8.0.0", + "stellar-sdk": "11.1.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/@soroban-react/xbull/node_modules/@soroban-react/types": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@soroban-react/types/-/types-8.0.0.tgz", + "integrity": "sha512-t8eohOlmb4LAD1e3bfI01KKK7oCcm468QKsKrD0fL67fnZYcx9K61tSCqwWEbFCWGMGPkwaOwI3sE/bYKKDMIQ==" + }, "node_modules/@stablelib/aead": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@stablelib/aead/-/aead-1.0.1.tgz", @@ -1478,9 +1728,9 @@ "integrity": "sha512-3gnPjAz78htgqsNEDkEsKHKosV2BF2iZkoHCNxpmZwUxiPsw+2VaXMed8RRMe0rGk3d5GZe7RrSba8zV80J3Ag==" }, "node_modules/@stellar/stellar-base": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-11.0.1.tgz", - "integrity": "sha512-VQh+1KEtFjegD6spx08+lENt8tQOkQQQZoLtqExjpRXyWlqDhEe+bXMlBTYKDc5MIynHyD42RPEib27UG17trA==", + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-12.0.1.tgz", + "integrity": "sha512-g6c27MNsDgEdUmoNQJn7zCWoCY50WHt0OIIOq3PhWaJRtUaT++qs1Jpb8+1bny2GmhtfRGOfPUFSyQBuHT9Mvg==", "dependencies": { "@stellar/js-xdr": "^3.1.1", "base32.js": "^0.1.0", @@ -1490,16 +1740,16 @@ "tweetnacl": "^1.0.3" }, "optionalDependencies": { - "sodium-native": "^4.0.10" + "sodium-native": "^4.1.1" } }, "node_modules/@stellar/stellar-sdk": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-11.3.0.tgz", - "integrity": "sha512-i+heopibJNRA7iM8rEPz0AXphBPYvy2HDo8rxbDwWpozwCfw8kglP9cLkkhgJe8YicgLrdExz/iQZaLpqLC+6w==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-12.1.0.tgz", + "integrity": "sha512-Va0hu9SaPezmMbO5eMwL5D15Wrx1AGWRtxayUDRWV2Fr3ynY58mvCZS1vsgNQ4kE8MZe3nBVKv6T9Kzqwgx1PQ==", "dependencies": { - "@stellar/stellar-base": "^11.0.1", - "axios": "^1.6.8", + "@stellar/stellar-base": "^12.0.1", + "axios": "^1.7.2", "bignumber.js": "^9.1.2", "eventsource": "^2.0.2", "randombytes": "^2.1.0", @@ -2321,9 +2571,9 @@ } }, "node_modules/axios": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", - "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -2357,7 +2607,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "devOptional": true }, "node_modules/base32.js": { "version": "0.1.0", @@ -2386,6 +2636,26 @@ } ] }, + "node_modules/big.js": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-6.2.1.tgz", + "integrity": "sha512-bCtHMwL9LeDIozFn+oNhhFoq+yQ3BNdnsLSASUxLciOb1vgvpHsIO1dsENiGMgbb4SkP5TrzWzRiLddn8ahVOQ==", + "engines": { + "node": "*" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/bigjs" + } + }, + "node_modules/bigint-conversion": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bigint-conversion/-/bigint-conversion-2.4.3.tgz", + "integrity": "sha512-eM76IXlhXQD6HAoE6A7QLQ3jdC04EJdjH3zrlU1Jtt4/jj+O/pMGjGR5FY8/55FOIBsK25kly0RoG4GA4iKdvg==", + "dependencies": { + "@juanelas/base64": "^1.1.2" + } + }, "node_modules/bignumber.js": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", @@ -2409,18 +2679,18 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, + "devOptional": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -2449,6 +2719,23 @@ "ieee754": "^1.2.1" } }, + "node_modules/bunyan": { + "version": "1.8.15", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz", + "integrity": "sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==", + "engines": [ + "node >=0.10.0" + ], + "bin": { + "bunyan": "bin/bunyan" + }, + "optionalDependencies": { + "dtrace-provider": "~0.8", + "moment": "^2.19.3", + "mv": "~2", + "safe-json-stringify": "~1" + } + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -2561,6 +2848,11 @@ "consola": "^3.2.3" } }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, "node_modules/clipboardy": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-4.0.0.tgz", @@ -2626,7 +2918,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "devOptional": true }, "node_modules/confbox": { "version": "0.1.7", @@ -2787,6 +3079,11 @@ "node": ">=0.10.0" } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" + }, "node_modules/decode-uri-component": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", @@ -2878,6 +3175,14 @@ "node": ">=0.10" } }, + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dijkstrajs": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", @@ -2916,6 +3221,30 @@ "csstype": "^3.0.2" } }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dtrace-provider": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", + "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "nan": "^2.14.0" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/duplexify": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", @@ -3686,9 +4015,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -4044,7 +4373,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -4207,7 +4535,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, + "devOptional": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -4725,6 +5053,11 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbi": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.0.tgz", + "integrity": "sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g==" + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -4775,6 +5108,11 @@ "node": ">=4.0" } }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4893,6 +5231,16 @@ "node": ">=8" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + }, "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", @@ -4998,7 +5346,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "devOptional": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5010,11 +5358,23 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, + "devOptional": true, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "optional": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mlly": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.0.tgz", @@ -5026,6 +5386,15 @@ "ufo": "^1.5.3" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "optional": true, + "engines": { + "node": "*" + } + }, "node_modules/motion": { "version": "10.16.2", "resolved": "https://registry.npmjs.org/motion/-/motion-10.16.2.tgz", @@ -5058,6 +5427,56 @@ "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==" }, + "node_modules/mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==", + "optional": true, + "dependencies": { + "mkdirp": "~0.5.1", + "ncp": "~2.0.0", + "rimraf": "~2.4.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/mv/node_modules/glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "optional": true, + "dependencies": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mv/node_modules/rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "optional": true, + "dependencies": { + "glob": "^6.0.1" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/nan": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", + "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", + "optional": true + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -5081,6 +5500,15 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", + "optional": true, + "bin": { + "ncp": "bin/ncp" + } + }, "node_modules/next": { "version": "12.3.4", "resolved": "https://registry.npmjs.org/next/-/next-12.3.4.tgz", @@ -5133,6 +5561,18 @@ } } }, + "node_modules/nise": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", + "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + } + }, "node_modules/node-addon-api": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.0.tgz", @@ -5174,9 +5614,9 @@ } }, "node_modules/node-gyp-build": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz", - "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", + "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", "optional": true, "bin": { "node-gyp-build": "bin.js", @@ -5473,7 +5913,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -5491,6 +5931,11 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, + "node_modules/path-to-regexp": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==" + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -6022,6 +6467,12 @@ } ] }, + "node_modules/safe-json-stringify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", + "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==", + "optional": true + }, "node_modules/safe-regex-test": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", @@ -6167,6 +6618,23 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.5", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -6194,6 +6662,56 @@ "atomic-sleep": "^1.0.0" } }, + "node_modules/soroswap-router-sdk": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/soroswap-router-sdk/-/soroswap-router-sdk-1.2.9.tgz", + "integrity": "sha512-rcN1KGh+gMFFTugGpbDhdsGV8ePB9IV3E7AzoeXaKr+spCU4UU0kcvpSmpG9BZwjjoQvWxf9gce7AI1KIEUDYg==", + "dependencies": { + "@stellar/stellar-sdk": "^11.3.0", + "axios": "^1.6.5", + "big.js": "^6.2.1", + "bigint-conversion": "^2.4.3", + "bignumber.js": "^9.1.2", + "bunyan": "^1.8.15", + "decimal.js-light": "^2.5.1", + "dotenv": "^16.4.0", + "jsbi": "^4.3.0", + "lodash": "^4.17.21", + "sinon": "^17.0.1", + "tiny-invariant": "^1.3.1", + "toformat": "^2.0.0" + } + }, + "node_modules/soroswap-router-sdk/node_modules/@stellar/stellar-base": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-11.0.1.tgz", + "integrity": "sha512-VQh+1KEtFjegD6spx08+lENt8tQOkQQQZoLtqExjpRXyWlqDhEe+bXMlBTYKDc5MIynHyD42RPEib27UG17trA==", + "dependencies": { + "@stellar/js-xdr": "^3.1.1", + "base32.js": "^0.1.0", + "bignumber.js": "^9.1.2", + "buffer": "^6.0.3", + "sha.js": "^2.3.6", + "tweetnacl": "^1.0.3" + }, + "optionalDependencies": { + "sodium-native": "^4.0.10" + } + }, + "node_modules/soroswap-router-sdk/node_modules/@stellar/stellar-sdk": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-11.3.0.tgz", + "integrity": "sha512-i+heopibJNRA7iM8rEPz0AXphBPYvy2HDo8rxbDwWpozwCfw8kglP9cLkkhgJe8YicgLrdExz/iQZaLpqLC+6w==", + "dependencies": { + "@stellar/stellar-base": "^11.0.1", + "axios": "^1.6.8", + "bignumber.js": "^9.1.2", + "eventsource": "^2.0.2", + "randombytes": "^2.1.0", + "toml": "^3.0.0", + "urijs": "^1.19.1" + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -6231,6 +6749,37 @@ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==" }, + "node_modules/stellar-sdk": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/stellar-sdk/-/stellar-sdk-11.1.0.tgz", + "integrity": "sha512-fIdo77ogpU+ecHgs59pk9velpXd4F/ch0DzOI4QZw8zVZApc3oeNWP3+X6ui7BWpeRHAGsP2CHQzBLxm0JTIgg==", + "deprecated": "⚠️ This package has moved to @stellar/stellar-sdk! 🚚", + "dependencies": { + "@stellar/stellar-base": "10.0.1", + "axios": "^1.6.0", + "bignumber.js": "^9.1.2", + "eventsource": "^2.0.2", + "randombytes": "^2.1.0", + "toml": "^3.0.0", + "urijs": "^1.19.1" + } + }, + "node_modules/stellar-sdk/node_modules/@stellar/stellar-base": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-10.0.1.tgz", + "integrity": "sha512-BDbx7VHOEQh+4J3Q+gStNXgPaNckVFmD4aOlBBGwxlF6vPFmVnW8IoJdkX7T58zpX55eWI6DXvEhDBlrqTlhAQ==", + "dependencies": { + "@stellar/js-xdr": "^3.0.1", + "base32.js": "^0.1.0", + "bignumber.js": "^9.1.2", + "buffer": "^6.0.3", + "sha.js": "^2.3.6", + "tweetnacl": "^1.0.3" + }, + "optionalDependencies": { + "sodium-native": "^4.0.1" + } + }, "node_modules/stream-shift": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", @@ -6416,7 +6965,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -6435,6 +6983,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swr": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.5.tgz", + "integrity": "sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==", + "dependencies": { + "client-only": "^0.0.1", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/system-architecture": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/system-architecture/-/system-architecture-0.1.0.tgz", @@ -6469,6 +7029,11 @@ "real-require": "^0.1.0" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -6488,6 +7053,11 @@ "node": ">=8.0" } }, + "node_modules/toformat": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/toformat/-/toformat-2.0.0.tgz", + "integrity": "sha512-03SWBVop6nU8bpyZCx7SodpYznbZF5R4ljwNLBcTQzKOD9xuihRo/psX58llS1BMFhhAI08H3luot5GoXJz2pQ==" + }, "node_modules/toggle-selection": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", @@ -6554,6 +7124,14 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -6989,9 +7567,9 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "engines": { "node": ">=8.3.0" }, diff --git a/package.json b/package.json index 863714f..f1e59eb 100644 --- a/package.json +++ b/package.json @@ -1,34 +1,40 @@ { "name": "blend-ui", - "version": "1.0.0", + "version": "1.0.1", "private": true, "type": "module", "scripts": { - "dev": "TARGET_ENV=testnet node loadEnv.js && next dev", + "dev": "next dev", "build": "TARGET_ENV=testnet node loadEnv.js && next build", "build:mainnet": "TARGET_ENV=production node loadEnv.js && next build", "build:testnet": "TARGET_ENV=testnet node loadEnv.js && next build", - "build-static": "next build && next export", + "build-static": "TARGET_ENV=testnet node loadEnv.js && next build && next export", "build-static:mainnet": "TARGET_ENV=production node loadEnv.js && next build && next export", "build-static:testnet": "TARGET_ENV=testnet node loadEnv.js && next build && next export", "start": "next start", "lint": "next lint" }, "dependencies": { - "@blend-capital/blend-sdk": "^1.1.0", - "@creit.tech/stellar-wallets-kit": "0.8.1", + "@blend-capital/blend-sdk": "1.2.0", + "@creit.tech/stellar-wallets-kit": "0.8.2", "@emotion/react": "^11.9.3", "@emotion/styled": "^11.9.3", "@mui/icons-material": "^5.8.4", "@mui/material": "^5.8.6", + "@soroban-react/contracts": "^9.1.10", + "@soroban-react/core": "^9.1.10", "@stellar/freighter-api": "^2.0.0", + "@stellar/stellar-sdk": "^12.1.0", + "axios": "^1.7.2", + "bigint-conversion": "^2.4.3", "bignumber.js": "^9.0.2", "copy-to-clipboard": "^3.3.3", "next": "^12.3.4", "react": "^18.2.0", "react-countdown": "^2.3.5", "react-dom": "^18.2.0", - "@stellar/stellar-sdk": "11.3.0", + "soroswap-router-sdk": "^1.2.9", + "swr": "^2.2.5", "zustand": "^4.3.7" }, "devDependencies": { @@ -42,4 +48,4 @@ "typescript": "4.7.4" }, "packageManager": "^npm@9.5.0" -} \ No newline at end of file +} diff --git a/public/icons/Orbit_Logo.svg b/public/icons/Orbit_Logo.svg new file mode 100644 index 0000000..c7b387d --- /dev/null +++ b/public/icons/Orbit_Logo.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/backstop/BackstopDepositAnvil.tsx b/src/components/backstop/BackstopDepositAnvil.tsx index a616129..0c35828 100644 --- a/src/components/backstop/BackstopDepositAnvil.tsx +++ b/src/components/backstop/BackstopDepositAnvil.tsx @@ -96,7 +96,7 @@ export const BackstopDepositAnvil: React.FC = ({ poolId }) = borderRadius: '5px', padding: '12px', marginBottom: '12px', - boxShadow: '0px 4px 4px rgba(0, 0, 0, 0.25)', + boxShadow: '0px 4px 4px rgba(0, 0, 0, 0.1)', }} > diff --git a/src/components/backstop/BackstopExitAnvil.tsx b/src/components/backstop/BackstopExitAnvil.tsx index 4189ca2..4038a5c 100644 --- a/src/components/backstop/BackstopExitAnvil.tsx +++ b/src/components/backstop/BackstopExitAnvil.tsx @@ -221,7 +221,7 @@ export const BackstopExitAnvil = () => { borderRadius: '5px', padding: '12px', marginBottom: '12px', - boxShadow: '0px 4px 4px rgba(0, 0, 0, 0.25)', + boxShadow: '0px 4px 4px rgba(0, 0, 0, 0.1)', }} > diff --git a/src/components/backstop/BackstopJoinAnvil.tsx b/src/components/backstop/BackstopJoinAnvil.tsx index 85c2ba0..333d858 100644 --- a/src/components/backstop/BackstopJoinAnvil.tsx +++ b/src/components/backstop/BackstopJoinAnvil.tsx @@ -356,7 +356,7 @@ export const BackstopJoinAnvil = () => { borderRadius: '5px', padding: '12px', marginBottom: '12px', - boxShadow: '0px 4px 4px rgba(0, 0, 0, 0.25)', + boxShadow: '0px 4px 4px rgba(0, 0, 0, 0.1)', }} > diff --git a/src/components/backstop/BackstopQueueAnvil.tsx b/src/components/backstop/BackstopQueueAnvil.tsx index 4ef7da6..028a895 100644 --- a/src/components/backstop/BackstopQueueAnvil.tsx +++ b/src/components/backstop/BackstopQueueAnvil.tsx @@ -101,7 +101,7 @@ export const BackstopQueueAnvil: React.FC = ({ poolId }) => borderRadius: '5px', padding: '12px', marginBottom: '12px', - boxShadow: '0px 4px 4px rgba(0, 0, 0, 0.25)', + boxShadow: '0px 4px 4px rgba(0, 0, 0, 0.1)', }} > diff --git a/src/components/backstop/BackstopQueueMod.tsx b/src/components/backstop/BackstopQueueMod.tsx index 2b84df7..9397138 100644 --- a/src/components/backstop/BackstopQueueMod.tsx +++ b/src/components/backstop/BackstopQueueMod.tsx @@ -16,8 +16,7 @@ export const BackstopQueueMod: React.FC = ({ poolId }) => { if ( !poolBackstopUserData || !poolBackstopUserEst || - poolBackstopUserData.q4w == undefined || - poolBackstopUserData.q4w.length == 0 + (poolBackstopUserData.totalQ4W == BigInt(0) && poolBackstopUserData.unlockedQ4W == BigInt(0)) ) { return <>; } diff --git a/src/components/borrow/BorrowAnvil.tsx b/src/components/borrow/BorrowAnvil.tsx index 241bd9c..d705036 100644 --- a/src/components/borrow/BorrowAnvil.tsx +++ b/src/components/borrow/BorrowAnvil.tsx @@ -6,7 +6,7 @@ import { SubmitArgs, UserPositions, } from '@blend-capital/blend-sdk'; -import { Box, CircularProgress, Typography, useTheme } from '@mui/material'; +import { Box, CircularProgress, Slider, TextField, Typography, useTheme } from '@mui/material'; import { SorobanRpc } from '@stellar/stellar-sdk'; import Image from 'next/image'; import { useMemo, useState } from 'react'; @@ -29,6 +29,10 @@ import { TxOverview } from '../common/TxOverview'; import { Value } from '../common/Value'; import { ValueChange } from '../common/ValueChange'; +const XLM_ADDRESS = 'CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC'; + +const USDC_ADDRESS = 'CAQCFVLOBK5GIULPNZRGATJJMIZL5BSP7X5YJVMGCPTUEPFM4AVSRCJU'; + export const BorrowAnvil: React.FC = ({ poolId, assetId }) => { const theme = useTheme(); const { viewType } = useSettings(); @@ -41,6 +45,8 @@ export const BorrowAnvil: React.FC = ({ poolId, assetId } const userAccount = useStore((state) => state.account); const [toBorrow, setToBorrow] = useState(''); + const [collateralRatio, setCollateralRatio] = useState(110); + const [collateralAmount, setCollateralAmount] = useState('0'); const [simResponse, setSimResponse] = useState(); const [parsedSimResult, setParsedSimResult] = useState(); const [loadingEstimate, setLoadingEstimate] = useState(false); @@ -147,6 +153,11 @@ export const BorrowAnvil: React.FC = ({ poolId, assetId } address: reserve.assetId, request_type: RequestType.Borrow, }, + { + amount: scaleInputToBigInt(toBorrow, reserve.config.decimals), + request_type: RequestType.SupplyCollateral, + address: reserve.assetId, + }, ], }; return await poolSubmit(poolId, submitArgs, sim); @@ -160,6 +171,24 @@ export const BorrowAnvil: React.FC = ({ poolId, assetId } } } + const handleCollateralChange = (event: any, value: number | number[]) => { + if (typeof value === 'number') { + const fixedValues = [110, 200, 300, 500]; + const closestFixedValue = fixedValues.reduce((prev, curr) => + Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev + ); + + if (fixedValues.includes(value)) { + setCollateralRatio(value); + } else { + setCollateralRatio(value); + } + + const newCollateralAmount = ((Number(toBorrow) * value) / 100).toFixed(2); + setCollateralAmount(newCollateralAmount); + } + }; + return (
= ({ poolId, assetId } borderRadius: '5px', padding: '12px', marginBottom: '12px', - boxShadow: '0px 4px 4px rgba(0, 0, 0, 0.25)', + boxShadow: '0px 4px 4px rgba(0, 0, 0, 0.1)', }} > @@ -196,7 +225,7 @@ export const BorrowAnvil: React.FC = ({ poolId, assetId } setLoadingEstimate(true); }} palette={theme.palette.borrow} - sx={{ width: '100%' }} + sx={{ width: '100%', background: '#F1F3F4' }} > = ({ poolId, assetId } )} + + + Collateral Ratio + + + + + + + + + + Info Fields + + + + + + + + + {!isError && ( {!isLoading && ( diff --git a/src/components/borrow/BorrowPositionCard.tsx b/src/components/borrow/BorrowPositionCard.tsx index 93f2f49..6b08dc2 100644 --- a/src/components/borrow/BorrowPositionCard.tsx +++ b/src/components/borrow/BorrowPositionCard.tsx @@ -43,7 +43,7 @@ export const BorrowPositionCard: React.FC = ({ backgroundColor: viewType == ViewType.MOBILE ? theme.palette.background.paper : 'inherit', padding: viewType == ViewType.MOBILE ? '1rem' : '0px', borderRadius: viewType == ViewType.MOBILE ? '6px' : '0px', - boxShadow: viewType === ViewType.MOBILE ? '0px 4px 4px rgba(0, 0, 0, 0.25)' : 'none', + boxShadow: viewType === ViewType.MOBILE ? '0px 4px 4px rgba(0, 0, 0, 0.1)' : 'none', }} onClick={() => { if (viewType === ViewType.MOBILE) { diff --git a/src/components/common/AnvilAlert.tsx b/src/components/common/AnvilAlert.tsx index 5183532..cb3d4e9 100644 --- a/src/components/common/AnvilAlert.tsx +++ b/src/components/common/AnvilAlert.tsx @@ -22,6 +22,7 @@ export function AnvilAlert({ severity, message, extraContent }: AnvilAlertProps) sx={{ display: 'flex', justifyContent: 'flex-start', + background: 'white', alignItems: !!extraContent ? 'start' : 'center', width: '100%', }} diff --git a/src/components/common/CoinSelectMenu.tsx b/src/components/common/CoinSelectMenu.tsx new file mode 100644 index 0000000..7d663cf --- /dev/null +++ b/src/components/common/CoinSelectMenu.tsx @@ -0,0 +1,113 @@ +import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; +import { Box, Button, Icon, Menu, MenuItem, TextField, useTheme } from '@mui/material'; +import Image from 'next/image'; +import React, { useEffect } from 'react'; + +interface CoinSelectMenuProps { + coins: string[]; + selectedCoin: string; + onSelectCoin: (coin: string) => void; + amount: string; + onAmountChange: (amount: string) => void; +} + +const coinIconPaths: { [key: string]: string } = { + XLM: '/icons/tokens/xlm.svg', // Sample path for XLM + USDC: '/icons/tokens/usdc.svg', // Sample path for USDC + // Add other coins and their respective paths here +}; + +const CoinSelectMenu: React.FC = ({ + coins, + selectedCoin, + onSelectCoin, + amount, + onAmountChange, +}) => { + const theme = useTheme(); + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + + const handleClickDropdown = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleSelectCoin = (coin: string) => { + onSelectCoin(coin); + handleClose(); + }; + + useEffect(() => { + if (!coins.includes(selectedCoin)) { + onSelectCoin(coins[0]); + } + }, [coins, selectedCoin, onSelectCoin]); + + return ( + + + + + {coins.map((coin) => ( + handleSelectCoin(coin)} key={coin}> + + {`${selectedCoin}`} + + {coin} + + ))} + + + onAmountChange(e.target.value)} + placeholder={'Amount'} + fullWidth + sx={{ + marginTop: '8px', + width: '80%', + '& .MuiInputBase-input': { + textAlign: 'right', + paddingRight: '10px', // Adjust this value as needed + }, + }} + /> + + ); +}; + +export default CoinSelectMenu; diff --git a/src/components/common/Divider.tsx b/src/components/common/Divider.tsx index 4994064..f6ffa81 100644 --- a/src/components/common/Divider.tsx +++ b/src/components/common/Divider.tsx @@ -5,8 +5,9 @@ export const Divider: React.FC = () => { return ( = ({ action, poolId, act const reserves = useStore((state) => state.pools.get(poolId)?.reserves); const activeReserve = reserves?.get(activeReserveId); + useEffect(() => { + console.log(activeReserveId); + }, [activeReserve]); + const [anchorEl, setAnchorEl] = React.useState(null); const open = Boolean(anchorEl); @@ -76,29 +80,59 @@ export const ReserveDropdown: React.FC = ({ action, poolId, act sx: { width: anchorEl && anchorEl.offsetWidth }, }} > - {Array.from(reserves?.values() ?? []).map((reserve) => ( - handleClickReserve(reserve.assetId)} - sx={{ - display: 'flex', - flexDirection: 'row', - justifyContent: 'flex-start', - alignItems: 'center', - borderRadius: '5px', - paddingLeft: '6px', - }} - > - - - {`${capitalizedAction} ${reserve?.tokenMetadata?.symbol ?? 'unknown'}`} - - - ))} + {action == 'supply' + ? Array.from(reserves?.values() ?? []).map( + (reserve) => + reserve.config.l_factor == 0 && ( + handleClickReserve(reserve.assetId)} + sx={{ + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-start', + alignItems: 'center', + borderRadius: '5px', + paddingLeft: '6px', + }} + > + + + {`${capitalizedAction} ${reserve?.tokenMetadata?.symbol ?? 'unknown'}`} + + + ) + ) + : Array.from(reserves?.values() ?? []).map( + (reserve) => + reserve.config.l_factor > 0 && ( + handleClickReserve(reserve.assetId)} + sx={{ + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-start', + alignItems: 'center', + borderRadius: '5px', + paddingLeft: '6px', + }} + > + + + {`${capitalizedAction} ${reserve?.tokenMetadata?.symbol ?? 'unknown'}`} + + + ) + )} ); diff --git a/src/components/common/SectionBase.tsx b/src/components/common/SectionBase.tsx index 433a8c3..5b8914a 100644 --- a/src/components/common/SectionBase.tsx +++ b/src/components/common/SectionBase.tsx @@ -7,7 +7,7 @@ export interface SectionBaseProps extends BoxProps { export const SectionBase: React.FC = ({ children, type, sx, ...props }) => { const theme = useTheme(); const color = type === 'alt' ? theme.palette.background.default : theme.palette.background.paper; - const boxShadow = type === 'alt' ? 'unset' : '0px 4px 4px rgba(0, 0, 0, 0.25);'; + const boxShadow = type === 'alt' ? 'unset' : '0px 4px 4px rgba(0, 0, 0, 0.1);'; return ( = React.forwardRef( variant="text" sx={{ background: theme.palette.background.paper, - color: theme.palette.common.white, + color: theme.palette.primary.main, '&:hover': { background: theme.palette.background.paper, color: palette.main }, ...sx, }} diff --git a/src/components/common/WalletWarning.tsx b/src/components/common/WalletWarning.tsx index 9edbbbf..9404795 100644 --- a/src/components/common/WalletWarning.tsx +++ b/src/components/common/WalletWarning.tsx @@ -15,9 +15,19 @@ export const WalletWarning = () => { const loadUserData = useStore((state) => state.loadUserData); const [openCon, setOpenCon] = React.useState(false); + const [openError, setOpenError] = React.useState(false); + + const handleConnectWallet = (successful: boolean) => { + if (successful) { + setOpenCon(true); + } else { + setOpenError(true); + } + }; const handleSnackClose = () => { setOpenCon(false); + setOpenError(false); }; useEffect(() => { @@ -59,8 +69,7 @@ export const WalletWarning = () => { ) : ( { - connect(); - setOpenCon(true); + connect(handleConnectWallet); }} palette={theme.palette.warning} sx={{ @@ -104,6 +113,27 @@ export const WalletWarning = () => { Wallet connected. + + + Unable to connect wallet. + + ); }; diff --git a/src/components/dashboard/PositionOverview.tsx b/src/components/dashboard/PositionOverview.tsx index 34cd2d1..1e9285e 100644 --- a/src/components/dashboard/PositionOverview.tsx +++ b/src/components/dashboard/PositionOverview.tsx @@ -246,7 +246,6 @@ export const PositionOverview: React.FC = ({ poolId }) => { - {renderClaimButton()} )} diff --git a/src/components/lend/LendAnvil.tsx b/src/components/lend/LendAnvil.tsx index 41eb3bf..24c7504 100644 --- a/src/components/lend/LendAnvil.tsx +++ b/src/components/lend/LendAnvil.tsx @@ -6,47 +6,57 @@ import { SubmitArgs, UserPositions, } from '@blend-capital/blend-sdk'; -import { Box, Typography, useTheme } from '@mui/material'; +import { Box, CircularProgress, Slider, TextField, Typography, useTheme } from '@mui/material'; import { SorobanRpc } from '@stellar/stellar-sdk'; -import Image from 'next/image'; import { useMemo, useState } from 'react'; import { useSettings, ViewType } from '../../contexts'; import { TxStatus, TxType, useWallet } from '../../contexts/wallet'; import { RPC_DEBOUNCE_DELAY, useDebouncedState } from '../../hooks/debounce'; import { useStore } from '../../store/store'; -import { toBalance, toPercentage } from '../../utils/formatter'; -import { getAssetReserve } from '../../utils/horizon'; +import { toBalance } from '../../utils/formatter'; +import { requiresTrustline } from '../../utils/horizon'; import { scaleInputToBigInt } from '../../utils/scval'; -import { getErrorFromSim } from '../../utils/txSim'; +import { getErrorFromSim, SubmitError } from '../../utils/txSim'; import { AnvilAlert } from '../common/AnvilAlert'; import { InputBar } from '../common/InputBar'; import { InputButton } from '../common/InputButton'; import { OpaqueButton } from '../common/OpaqueButton'; import { ReserveComponentProps } from '../common/ReserveComponentProps'; +import { ReserveDropdown } from '../common/ReserveDropdown'; import { Row } from '../common/Row'; import { Section, SectionSize } from '../common/Section'; +import { StackedText } from '../common/StackedText'; import { TxOverview } from '../common/TxOverview'; -import { Value } from '../common/Value'; -import { ValueChange } from '../common/ValueChange'; + +const XLM_ADDRESS = 'CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC'; + +const OUSD_ADDRESS = 'CBGO6D5Q3SIPG6QHN2MJ5LQQ6XH2SRPKEB6PLRPS3KWDDPLBMDETEZRK'; export const LendAnvil: React.FC = ({ poolId, assetId }) => { const theme = useTheme(); const { viewType } = useSettings(); - const { connected, walletAddress, poolSubmit, txStatus, txType, isLoading } = useWallet(); + const { connected, walletAddress, poolSubmit, txStatus, txType, createTrustline, isLoading } = + useWallet(); - const account = useStore((state) => state.account); const poolData = useStore((state) => state.pools.get(poolId)); const userPoolData = useStore((state) => state.userPoolData.get(poolId)); - const userBalance = useStore((state) => state.balances.get(assetId)) ?? BigInt(0); + const userAccount = useStore((state) => state.account); - const [toLend, setToLend] = useState(''); + const [toBorrow, setToBorrow] = useState(''); + const [toSupply, setToSupply] = useState(''); + const [collateralRatio, setCollateralRatio] = useState(135); + const [collateralAmount, setCollateralAmount] = useState('0'); const [simResponse, setSimResponse] = useState(); const [parsedSimResult, setParsedSimResult] = useState(); const [loadingEstimate, setLoadingEstimate] = useState(false); const loading = isLoading || loadingEstimate; - useDebouncedState(toLend, RPC_DEBOUNCE_DELAY, txType, async () => { + if (txStatus === TxStatus.SUCCESS && txType === TxType.CONTRACT && Number(toBorrow) != 0) { + setToBorrow(''); + } + + useDebouncedState(toBorrow, RPC_DEBOUNCE_DELAY, txType, async () => { setSimResponse(undefined); setParsedSimResult(undefined); let response = await handleSubmitTransaction(true); @@ -62,14 +72,25 @@ export const LendAnvil: React.FC = ({ poolId, assetId }) let newPositionEstimate = poolData && parsedSimResult ? PositionEstimates.build(poolData, parsedSimResult) : undefined; - const reserve = poolData?.reserves.get(assetId); + const reserve = poolData?.reserves.get(OUSD_ADDRESS); + const reserve_xlm = poolData?.reserves.get(XLM_ADDRESS); + const assetToXlm = reserve_xlm?.oraclePrice ?? 1; + const assetToBase = reserve?.oraclePrice ?? 1; const decimals = reserve?.config.decimals ?? 7; - const scalar = 10 ** decimals; const symbol = reserve?.tokenMetadata?.symbol ?? ''; - const curBorrowCap = userPoolData ? userPoolData.positionEstimates.borrowCap : undefined; - const nextBorrowCap = newPositionEstimate ? newPositionEstimate.borrowCap : undefined; + const assetToEffectiveLiability = reserve + ? assetToBase * reserve.getLiabilityFactor() + : undefined; + const curBorrowCap = + userPoolData && assetToEffectiveLiability + ? userPoolData.positionEstimates.borrowCap / assetToEffectiveLiability + : undefined; + const nextBorrowCap = + newPositionEstimate && assetToEffectiveLiability + ? newPositionEstimate.borrowCap / assetToEffectiveLiability + : undefined; const curBorrowLimit = userPoolData && Number.isFinite(userPoolData?.positionEstimates.borrowLimit) ? userPoolData?.positionEstimates?.borrowLimit @@ -78,40 +99,87 @@ export const LendAnvil: React.FC = ({ poolId, assetId }) newPositionEstimate && Number.isFinite(newPositionEstimate?.borrowLimit) ? newPositionEstimate?.borrowLimit : 0; - - // calculate current wallet state - let stellar_reserve_amount = getAssetReserve(account, reserve?.tokenMetadata?.asset); - const freeUserBalanceScaled = Number(userBalance) / scalar - stellar_reserve_amount; - - if (txStatus === TxStatus.SUCCESS && txType === TxType.CONTRACT && Number(toLend) != 0) { - setToLend(''); - } - - const { isSubmitDisabled, isMaxDisabled, reason, disabledType, isError, extraContent } = useMemo( - () => getErrorFromSim(toLend, decimals, loading, simResponse, undefined), - [freeUserBalanceScaled, toLend, simResponse, loading] + const AddTrustlineButton = ( + + Add {reserve?.tokenMetadata.asset?.code} Trustline + ); - const handleLendMax = () => { - if (userPoolData) { - if (freeUserBalanceScaled > 0) { - setToLend(freeUserBalanceScaled.toFixed(decimals)); - setLoadingEstimate(true); + const { isSubmitDisabled, isMaxDisabled, reason, disabledType, extraContent, isError } = + useMemo(() => { + const hasTokenTrustline = !requiresTrustline(userAccount, reserve?.tokenMetadata?.asset); + if (!hasTokenTrustline) { + let submitError: SubmitError = { + isSubmitDisabled: true, + isError: true, + isMaxDisabled: true, + reason: 'You need a trustline for this asset in order to borrow it.', + disabledType: 'warning', + extraContent: AddTrustlineButton, + }; + return submitError; + } else { + return getErrorFromSim(toBorrow, decimals, loading, simResponse, undefined); } + }, [toBorrow, simResponse, userPoolData?.positionEstimates]); + + const handleBorrowMax = () => { + if (reserve && userPoolData) { + let to_bounded_hf = + (userPoolData.positionEstimates.totalEffectiveCollateral - + userPoolData.positionEstimates.totalEffectiveLiabilities * 1.02) / + 1.02; + let to_borrow = Math.min( + to_bounded_hf / (assetToBase * reserve.getLiabilityFactor()), + reserve.estimates.supplied * (reserve.config.max_util / 1e7 - 0.01) - + reserve.estimates.borrowed + ); + setToBorrow(Math.max(to_borrow, 0).toFixed(7)); + setLoadingEstimate(true); } }; + const handleChangeBorrow = (value: string) => { + setToBorrow(value); + // calculate price of usd to xlm and apply collateral ratio + const supplyAmount = ((Number(value) / assetToXlm) * (collateralRatio / 100)).toFixed(7); + setToSupply(supplyAmount); + setLoadingEstimate(true); + }; + + const handleChangeSupply = (value: string) => { + setToSupply(value); + const borrowAmount = ( + (Number(toSupply) * assetToXlm) / + assetToBase / + (collateralRatio / 100) + ).toFixed(7); + setToBorrow(borrowAmount); + setLoadingEstimate(true); + }; + const handleSubmitTransaction = async (sim: boolean) => { - if (toLend && connected && reserve) { + console.log(poolData?.reserves); + if (toBorrow && connected && reserve_xlm) { let submitArgs: SubmitArgs = { from: walletAddress, - spender: walletAddress, to: walletAddress, + spender: walletAddress, + requests: [ { - amount: scaleInputToBigInt(toLend, reserve.config.decimals), + amount: scaleInputToBigInt(toSupply, reserve_xlm.config.decimals), request_type: RequestType.SupplyCollateral, - address: reserve.assetId, + address: XLM_ADDRESS, + }, + { + amount: scaleInputToBigInt(toBorrow, reserve_xlm.config.decimals), + address: OUSD_ADDRESS, + request_type: RequestType.Borrow, }, ], }; @@ -119,6 +187,24 @@ export const LendAnvil: React.FC = ({ poolId, assetId }) } }; + async function handleAddAssetTrustline() { + if (connected && reserve?.tokenMetadata?.asset) { + const reserveAsset = reserve?.tokenMetadata?.asset; + await createTrustline(reserveAsset); + } + } + + const handleCollateralChange = (event: any, value: number | number[]) => { + if (typeof value === 'number') { + setCollateralRatio(value); + + setToSupply(((Number(toBorrow) / assetToXlm) * (value / 100)).toFixed(2)); + + const newCollateralAmount = ((Number(toBorrow) * value) / 100).toFixed(2); + setCollateralAmount(newCollateralAmount); + } + }; + return (
= ({ poolId, assetId }) > + + { + handleChangeBorrow(v); + setLoadingEstimate(true); + }} + palette={theme.palette.borrow} + sx={{ width: '100%', background: '#F1F3F4' }} + > + + + + {`$${toBalance(Number(toBorrow ?? 0), decimals)}`} + + {viewType === ViewType.MOBILE && ( + handleSubmitTransaction(false)} + palette={theme.palette.borrow} + sx={{ minWidth: '108px', width: '100%', padding: '6px' }} + disabled={isSubmitDisabled} + > + Borrow + + )} + + + - Amount to supply + Collateral Ratio + + + { + setCollateralAmount(((Number(toBorrow) * collateralRatio) / 100).toFixed(2)); + setCollateralRatio(Number(e.target.value)); + }} + variant="outlined" + sx={{ minWidth: '120px' }} + /> + + + + +
+ +
+
+ = ({ poolId, assetId }) }} > { - setToLend(v); + handleChangeSupply(v); setLoadingEstimate(true); }} - sx={{ width: '100%' }} - palette={theme.palette.lend} + palette={theme.palette.borrow} + sx={{ width: '100%', background: '#F1F3F4' }} > - {viewType !== ViewType.MOBILE && ( - handleSubmitTransaction(false)} - palette={theme.palette.lend} - sx={{ minWidth: '108px', marginLeft: '12px', padding: '6px' }} - disabled={isSubmitDisabled} - > - Supply - - )} - + - {`$${toBalance(Number(toLend ?? 0) * assetToBase, decimals)}`} + {`$${toBalance(Number(toSupply ?? 0) * assetToXlm, decimals)}`} {viewType === ViewType.MOBILE && ( handleSubmitTransaction(false)} - palette={theme.palette.lend} + palette={theme.palette.borrow} sx={{ minWidth: '108px', width: '100%', padding: '6px' }} disabled={isSubmitDisabled} > - Supply + Borrow )} + {!isError && ( - - <> - - - blend{' '} - Gas - - } - value={`${toBalance( - BigInt((simResponse as any)?.minResourceFee ?? 0), - decimals - )} XLM`} - /> - - - - + + {!isLoading && ( + <> + + + +
+ +
+
+ +
+
+ +
+
+
+
+ handleSubmitTransaction(false)} + palette={theme.palette.primary.dark} + sx={{ minWidth: '108px', padding: '10px' }} + disabled={isSubmitDisabled} + > + Borrow + + + )} + {isLoading && ( + + + + )}
)} {isError && ( diff --git a/src/components/lend/LendPositionCard.tsx b/src/components/lend/LendPositionCard.tsx index 2612705..a1b9370 100644 --- a/src/components/lend/LendPositionCard.tsx +++ b/src/components/lend/LendPositionCard.tsx @@ -46,7 +46,7 @@ export const LendPositionCard: React.FC = ({ backgroundColor: viewType == ViewType.MOBILE ? theme.palette.background.paper : 'inherit', padding: viewType == ViewType.MOBILE ? '1rem' : '0px', borderRadius: viewType == ViewType.MOBILE ? '6px' : '0px', - boxShadow: viewType === ViewType.MOBILE ? '0px 4px 4px rgba(0, 0, 0, 0.25)' : 'none', + boxShadow: viewType === ViewType.MOBILE ? '0px 4px 4px rgba(0, 0, 0, 0.1)' : 'none', }} onClick={() => { if (viewType === ViewType.MOBILE) { diff --git a/src/components/markets/MarketCard.tsx b/src/components/markets/MarketCard.tsx index c187c96..ba7addc 100644 --- a/src/components/markets/MarketCard.tsx +++ b/src/components/markets/MarketCard.tsx @@ -19,7 +19,6 @@ export const MarketCard: React.FC = ({ poolId, sx }) => { const poolData = useStore((state) => state.pools.get(poolId)); const backstopPoolData = useStore((state) => state.backstop?.pools.get(poolId)); - console.log('poolData', useStore((state) => state.backstop)); const [expand, setExpand] = useState(false); const [rotateArrow, setRotateArrow] = useState(false); diff --git a/src/components/nav/NavBar.tsx b/src/components/nav/NavBar.tsx index 128ce9e..7219976 100644 --- a/src/components/nav/NavBar.tsx +++ b/src/components/nav/NavBar.tsx @@ -32,16 +32,16 @@ export const NavBar = () => { return ( - + - Blend Logo + Blend Logo {viewType === ViewType.REGULAR && ( { }} >
-
@@ -67,7 +66,14 @@ export const NavBar = () => { )} {viewType !== ViewType.REGULAR && ( - + )} diff --git a/src/components/nav/NavMenu.tsx b/src/components/nav/NavMenu.tsx index b91499d..8262815 100644 --- a/src/components/nav/NavMenu.tsx +++ b/src/components/nav/NavMenu.tsx @@ -83,11 +83,6 @@ export const NavMenu = () => { GitHub - - - Terms of Service - - )} {viewType !== ViewType.REGULAR && ( @@ -137,11 +132,6 @@ export const NavMenu = () => { GitHub - - - Terms of Service - - )} diff --git a/src/components/nav/WalletMenu.tsx b/src/components/nav/WalletMenu.tsx index a92f45b..fb8ed3a 100644 --- a/src/components/nav/WalletMenu.tsx +++ b/src/components/nav/WalletMenu.tsx @@ -23,10 +23,19 @@ export const WalletMenu = () => { const theme = useTheme(); const { connect, disconnect, connected, walletAddress, isLoading } = useWallet(); - //snackbars + // snackbars const [openCon, setOpenCon] = React.useState(false); const [openDis, setOpenDis] = React.useState(false); const [openCopy, setOpenCopy] = React.useState(false); + const [openError, setOpenError] = React.useState(false); + + const handleConnectWallet = (successful: boolean) => { + if (successful) { + setOpenCon(true); + } else { + setOpenError(true); + } + }; const handleDisconnectWallet = () => { disconnect(); @@ -42,6 +51,7 @@ export const WalletMenu = () => { setOpenCon(false); setOpenDis(false); setOpenCopy(false); + setOpenError(false); }; const [anchorElDropdown, setAnchorElDropdown] = React.useState(null); @@ -52,10 +62,11 @@ export const WalletMenu = () => { }; const handleClickConnect = () => { - connect(); + connect(handleConnectWallet); }; const handleClose = () => { + handleSnackClose(); setAnchorElDropdown(null); }; @@ -188,6 +199,27 @@ export const WalletMenu = () => { Wallet address copied to clipboard. + + + Unable to connect wallet. + + ); }; diff --git a/src/components/repay/RepayAnvil.tsx b/src/components/repay/RepayAnvil.tsx index eee301a..222cee9 100644 --- a/src/components/repay/RepayAnvil.tsx +++ b/src/components/repay/RepayAnvil.tsx @@ -139,7 +139,7 @@ export const RepayAnvil: React.FC = ({ poolId, assetId }) borderRadius: '5px', padding: '12px', marginBottom: '12px', - boxShadow: '0px 4px 4px rgba(0, 0, 0, 0.25)', + boxShadow: '0px 4px 4px rgba(0, 0, 0, 0.1)', }} > diff --git a/src/components/swap/SwapCurrencyInputPanel.tsx b/src/components/swap/SwapCurrencyInputPanel.tsx new file mode 100644 index 0000000..716f195 --- /dev/null +++ b/src/components/swap/SwapCurrencyInputPanel.tsx @@ -0,0 +1,51 @@ +import { Box, InputBase, MenuItem, Select, Typography } from '@mui/material'; +import { TokenIcon } from '../common/TokenIcon'; + +interface SwapCurrencyInputPanelProps { + label: React.ReactNode; + value: string; + onUserInput: (value: string) => void; + onCurrencySelect: (currency: string) => void; + currency: string; + coins: string[]; +} + +const SwapCurrencyInputPanel: React.FC = ({ + label, + value, + onUserInput, + onCurrencySelect, + currency, + coins, +}) => { + return ( + + + {label} + + + onUserInput(e.target.value)} + sx={{ + flexGrow: 1, + marginRight: '12px', + padding: '6px', + border: '1px solid #ccc', + borderRadius: '4px', + }} + /> + + + + ); +}; + +export default SwapCurrencyInputPanel; diff --git a/src/components/swap/styled.tsx b/src/components/swap/styled.tsx new file mode 100644 index 0000000..6674dc0 --- /dev/null +++ b/src/components/swap/styled.tsx @@ -0,0 +1,45 @@ +import { styled } from '@mui/material/styles'; + +export const SwapSection = styled('div')(({ theme }) => ({ + position: 'relative', + backgroundColor: theme.palette.background.paper, + borderRadius: 12, + padding: 16, + color: theme.palette.text.primary, + fontSize: 14, + lineHeight: '20px', + fontWeight: 500, + '&:before': { + boxSizing: 'border-box', + backgroundSize: '100%', + borderRadius: 'inherit', + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + pointerEvents: 'none', + border: `1px solid ${theme.palette.background.paper}`, + }, + '&:hover:before': { + borderColor: theme.palette.primary.main, + }, + '&:focus-within:before': { + borderColor: theme.palette.primary.light, + }, +})); + +export const OutputSwapSection = styled(SwapSection)` + border-bottom: ${({ theme }) => `1px solid ${theme.palette.background.paper}`}; + border-radius: 16px; + border: 1px solid rgba(180, 239, 175, 0.2); + background: ${({ theme }) => theme.palette.background.default}; +`; + +export const ArrowContainer = styled('div')` + display: inline-flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; +`; diff --git a/src/components/withdraw/WithdrawAnvil.tsx b/src/components/withdraw/WithdrawAnvil.tsx index a53f90e..d889ab9 100644 --- a/src/components/withdraw/WithdrawAnvil.tsx +++ b/src/components/withdraw/WithdrawAnvil.tsx @@ -181,7 +181,7 @@ export const WithdrawAnvil: React.FC = ({ poolId, assetId borderRadius: '5px', padding: '12px', marginBottom: '12px', - boxShadow: '0px 4px 4px rgba(0, 0, 0, 0.25)', + boxShadow: '0px 4px 4px rgba(0, 0, 0, 0.1)', }} > diff --git a/src/contexts/wallet.tsx b/src/contexts/wallet.tsx index 5084d59..2ad09c0 100644 --- a/src/contexts/wallet.tsx +++ b/src/contexts/wallet.tsx @@ -46,7 +46,7 @@ export interface IWalletContext { walletId: string | undefined; isLoading: boolean; - connect: () => Promise; + connect: (handleSuccess: (success: boolean) => void) => Promise; disconnect: () => void; clearLastTx: () => void; restore: (sim: SorobanRpc.Api.SimulateTransactionRestoreResponse) => Promise; @@ -164,37 +164,41 @@ export const WalletProvider = ({ children = null as any }) => { /** * Connect a wallet to the application via the walletKit */ - async function handleSetWalletAddress() { + async function handleSetWalletAddress(): Promise { try { const publicKey = await walletKit.getPublicKey(); + if (publicKey === '' || publicKey == undefined) { + console.error('Unable to load wallet key: ', publicKey); + return false; + } setWalletAddress(publicKey); setConnected(true); await loadUserData(publicKey); - setLoading(false); + return true; } catch (e: any) { - setLoading(false); console.error('Unable to load wallet information: ', e); + return false; } } /** * Open up a modal to connect the user's browser wallet */ - async function connect() { + async function connect(handleSuccess: (success: boolean) => void) { try { setLoading(true); await walletKit.openModal({ onWalletSelected: async (option: ISupportedWallet) => { walletKit.setWallet(option.id); setAutoConnect(option.id); - await handleSetWalletAddress(); - }, - onClosed: () => { - setLoading(false); + let result = await handleSetWalletAddress(); + handleSuccess(result); }, }); + setLoading(false); } catch (e: any) { setLoading(false); + handleSuccess(false); console.error('Unable to connect wallet: ', e); } } diff --git a/src/functions/getCurrentTimePlusOneHour.ts b/src/functions/getCurrentTimePlusOneHour.ts new file mode 100644 index 0000000..33f7701 --- /dev/null +++ b/src/functions/getCurrentTimePlusOneHour.ts @@ -0,0 +1,9 @@ +export const getCurrentTimePlusOneHour = () => { + // Get the current time in milliseconds + const now = Date.now(); + + // Add one hour (3600000 milliseconds) + const oneHourLater = now + 3600000; + + return oneHourLater; +}; diff --git a/src/helpers/aggregator/index.ts b/src/helpers/aggregator/index.ts new file mode 100644 index 0000000..56143c0 --- /dev/null +++ b/src/helpers/aggregator/index.ts @@ -0,0 +1,36 @@ +import { Address, nativeToScVal, xdr } from "@stellar/stellar-sdk"; + +export interface DexDistribution { + protocol_id: string; + path: string[], + parts: number, + is_exact_in: boolean, +} + +export const dexDistributionParser = (dexDistributionRaw: DexDistribution[]) : xdr.ScVal => { + + const dexDistributionScVal = dexDistributionRaw.map((distribution) => { + return xdr.ScVal.scvMap([ + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol('is_exact_in'), + val: xdr.ScVal.scvBool(distribution.is_exact_in), + }), + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol('parts'), + val: nativeToScVal(distribution.parts, {type: "i128"}), + }), + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol('path'), + val: nativeToScVal(distribution.path.map((pathAddress) => new Address(pathAddress))), + }), + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol('protocol_id'), + val: xdr.ScVal.scvString(distribution.protocol_id), + }), + ]); + }); + + return xdr.ScVal.scvVec(dexDistributionScVal) +} + +export const hasDistribution = (trade: any): trade is { distribution: DexDistribution[] } => trade && 'distribution' in trade; diff --git a/src/helpers/convert.ts b/src/helpers/convert.ts new file mode 100644 index 0000000..ffa3021 --- /dev/null +++ b/src/helpers/convert.ts @@ -0,0 +1,170 @@ +import { Address, xdr } from '@stellar/stellar-sdk'; +import { bufToBigint } from 'bigint-conversion'; +import { Buffer } from 'buffer'; +import { I128 } from './xdr'; + +export const decodei128ScVal = (value: any) => { + try { + return new I128([ + BigInt(value.i128().lo().low), + BigInt(value.i128().lo().high), + BigInt(value.i128().hi().low), + BigInt(value.i128().hi().high), + ]).toString(); + } catch (error) { + return 0; + } +}; + +export function scvalToBigInt(scval: xdr.ScVal | undefined): BigInt { + switch (scval?.switch()) { + case undefined: { + return BigInt(0); + } + case xdr.ScValType.scvU64(): { + const { high, low } = scval.u64(); + return bufToBigint(new Uint32Array([high, low])); + } + case xdr.ScValType.scvI64(): { + const { high, low } = scval.i64(); + return bufToBigint(new Int32Array([high, low])); + } + case xdr.ScValType.scvU128(): { + const parts = scval.u128(); + const a = parts.hi(); + const b = parts.lo(); + return decodei128ScVal(scval); + // return bufToBigint(new Uint32Array([a.high, a.low, b.high, b.low])); + } + case xdr.ScValType.scvI128(): { + const parts = scval.i128(); + const a = parts.hi(); + const b = parts.lo(); + + return decodei128ScVal(scval); + // return bufToBigint(new Int32Array([a.high, a.low, b.high, b.low])); + } + default: { + throw new Error(`Invalid type for scvalToBigInt: ${scval?.switch().name}`); + } + } +} + +export function strToScVal(base64Xdr: string): xdr.ScVal { + return xdr.ScVal.fromXDR(Buffer.from(base64Xdr, 'base64')); +} + +export function scValStrToJs(base64Xdr: string): T { + return scValToJs(strToScVal(base64Xdr)); +} + +export function scValToJs(val: xdr.ScVal): T { + switch (val?.switch()) { + case xdr.ScValType.scvBool(): { + return val.b() as unknown as T; + } + case xdr.ScValType.scvVoid(): + case undefined: { + return 0 as unknown as T; + } + case xdr.ScValType.scvU32(): { + return val.u32() as unknown as T; + } + case xdr.ScValType.scvI32(): { + return val.i32() as unknown as T; + } + case xdr.ScValType.scvU64(): + case xdr.ScValType.scvI64(): + case xdr.ScValType.scvU128(): + case xdr.ScValType.scvI128(): + case xdr.ScValType.scvU256(): + case xdr.ScValType.scvI256(): { + return scvalToBigInt(val) as unknown as T; + } + case xdr.ScValType.scvAddress(): { + return Address.fromScVal(val).toString() as unknown as T; + } + case xdr.ScValType.scvString(): { + return val.str().toString() as unknown as T; + } + case xdr.ScValType.scvSymbol(): { + return val.sym().toString() as unknown as T; + } + case xdr.ScValType.scvBytes(): { + return val.bytes() as unknown as T; + } + case xdr.ScValType.scvVec(): { + type Element = ElementType; + return val?.vec()?.map((v) => scValToJs(v)) as unknown as T; + } + case xdr.ScValType.scvMap(): { + type Key = KeyType; + type Value = ValueType; + let res: any = {}; + val?.map()?.forEach((e) => { + let key = scValToJs(e.key()); + let value; + let v: xdr.ScVal = e.val(); + // For now we assume second level maps are real maps. Not perfect but better. + switch (v?.switch()) { + case xdr.ScValType.scvMap(): { + let inner_map = new Map() as Map; + v?.map()?.forEach((e) => { + let key = scValToJs(e.key()); + let value = scValToJs(e.val()); + inner_map.set(key, value); + }); + value = inner_map; + break; + } + default: { + value = scValToJs(e.val()); + } + } + //@ts-ignore + res[key as Key] = value as Value; + }); + return res as unknown as T; + } + case xdr.ScValType.scvContractInstance(): + return val.instance() as unknown as T; + case xdr.ScValType.scvLedgerKeyNonce(): + return val.nonceKey() as unknown as T; + case xdr.ScValType.scvTimepoint(): + return val.timepoint() as unknown as T; + case xdr.ScValType.scvDuration(): + return val.duration() as unknown as T; + // TODO: Add this case when merged + // case xdr.ScValType.scvError(): + default: { + throw new Error(`type not implemented yet: ${val?.switch().name}`); + } + } +} + +type ElementType = T extends Array ? U : never; +type KeyType = T extends Map ? K : never; +type ValueType = T extends Map ? V : never; + +export function addressToScVal(addr: string): xdr.ScVal { + let addrObj = Address.fromString(addr); + return addrObj.toScVal(); +} + +export function i128ToScVal(i: bigint): xdr.ScVal { + return xdr.ScVal.scvI128( + new xdr.Int128Parts({ + lo: xdr.Uint64.fromString((i & BigInt('0xffffffffffffffff')).toString()), + hi: xdr.Int64.fromString(((i >> BigInt('64')) & BigInt('0xffffffffffffffff')).toString()), + }) + ); +} + +export function u128ToScVal(i: bigint): xdr.ScVal { + return xdr.ScVal.scvU128( + new xdr.UInt128Parts({ + lo: xdr.Uint64.fromString((i & BigInt('0xffffffffffffffff')).toString()), + hi: xdr.Int64.fromString(((i >> BigInt('64')) & BigInt('0xffffffffffffffff')).toString()), + }) + ); +} diff --git a/src/helpers/utils.tsx b/src/helpers/utils.tsx new file mode 100644 index 0000000..b8b3cae --- /dev/null +++ b/src/helpers/utils.tsx @@ -0,0 +1,205 @@ +import * as StellarSdk from '@stellar/stellar-sdk'; +import BigNumber from 'bignumber.js'; +import { I128 } from './xdr'; + +let xdr = StellarSdk.xdr; + +export function scvalToBigNumber(scval: StellarSdk.xdr.ScVal | undefined): BigNumber { + switch (scval?.switch()) { + case undefined: { + return BigNumber(0); + } + case xdr.ScValType.scvU32(): { + return BigNumber(scval.u32()); + } + case xdr.ScValType.scvI32(): { + return BigNumber(scval.i32()); + } + case xdr.ScValType.scvU64(): { + const { high, low } = scval.u64(); + return bigNumberFromBytes(false, high, low); + } + case xdr.ScValType.scvI64(): { + const { high, low } = scval.i64(); + return bigNumberFromBytes(true, high, low); + } + case xdr.ScValType.scvU128(): { + const parts = scval.u128(); + const hi = parts.hi(); + const lo = parts.lo(); + return bigNumberFromBytes(false, lo.low, lo.high, hi.low, hi.high); + } + case xdr.ScValType.scvI128(): { + return BigNumber(decodei128ScVal(scval)); + } + case xdr.ScValType.scvU256(): { + const parts = scval.u256(); + const a = parts.hiHi(); + const b = parts.hiLo(); + const c = parts.loHi(); + const d = parts.loLo(); + return bigNumberFromBytes(false, a.high, a.low, b.high, b.low, c.high, c.low, d.high, d.low); + } + case xdr.ScValType.scvI256(): { + const parts = scval.i256(); + const a = parts.hiHi(); + const b = parts.hiLo(); + const c = parts.loHi(); + const d = parts.loLo(); + return bigNumberFromBytes(true, a.high, a.low, b.high, b.low, c.high, c.low, d.high, d.low); + } + default: { + throw new Error(`Invalid type for scvalToBigNumber: ${scval?.switch().name}`); + } + } +} + +function bigNumberFromBytes(signed: boolean, ...bytes: (string | number | bigint)[]): BigNumber { + let sign = 1; + if (signed && bytes[0] === 0x80) { + // top bit is set, negative number. + sign = -1; + bytes[0] &= 0x7f; + } + let b = BigInt(0); + for (let byte of bytes) { + b <<= BigInt(8); + b |= BigInt(byte); + } + return BigNumber(b.toString()).multipliedBy(sign); +} + +export function bigNumberToI128(value: BigNumber): StellarSdk.xdr.ScVal { + const b: bigint = BigInt(value.toFixed(0)); + const buf = bigintToBuf(b); + if (buf.length > 16) { + throw new Error('BigNumber overflows i128'); + } + + if (value.isNegative()) { + // Clear the top bit + buf[0] &= 0x7f; + } + + // left-pad with zeros up to 16 bytes + let padded = Buffer.alloc(16); + buf.copy(padded, padded.length - buf.length); + + if (value.isNegative()) { + // Set the top bit + padded[0] |= 0x80; + } + + const hi = new xdr.Int64([ + bigNumberFromBytes(false, ...padded.slice(4, 8)).toNumber(), + bigNumberFromBytes(false, ...padded.slice(0, 4)).toNumber(), + ]); + const lo = new xdr.Uint64([ + bigNumberFromBytes(false, ...padded.slice(12, 16)).toNumber(), + bigNumberFromBytes(false, ...padded.slice(8, 12)).toNumber(), + ]); + + return xdr.ScVal.scvI128(new xdr.Int128Parts({ lo, hi })); +} + +function bigintToBuf(bn: bigint): Buffer { + var hex = BigInt(bn).toString(16).replace(/^-/, ''); + if (hex.length % 2) { + hex = '0' + hex; + } + + var len = hex.length / 2; + var u8 = new Uint8Array(len); + + var i = 0; + var j = 0; + while (i < len) { + u8[i] = parseInt(hex.slice(j, j + 2), 16); + i += 1; + j += 2; + } + + if (bn < BigInt(0)) { + // Set the top bit + u8[0] |= 0x80; + } + + return Buffer.from(u8); +} + +export function xdrUint64ToNumber(value: StellarSdk.xdr.Uint64): number { + let b = 0; + b |= value.high; + b <<= 8; + b |= value.low; + return b; +} + +export function scvalToString(value: StellarSdk.xdr.ScVal): string | undefined { + return value.bytes().toString(); +} + +// XDR -> String +export const decodei128ScVal = (value: StellarSdk.xdr.ScVal) => { + try { + return new I128([ + BigInt(value.i128().lo().low), + BigInt(value.i128().lo().high), + BigInt(value.i128().hi().low), + BigInt(value.i128().hi().high), + ]).toString(); + } catch (error) { + return 0; + } +}; + +export function accountToScVal(account: string): StellarSdk.xdr.ScVal { + return new StellarSdk.Address(account).toScVal(); +} + +export function contractAddressToScVal(contractAddress: string): any { + return StellarSdk.Address.contract(Buffer.from(contractAddress, 'hex')).toScVal(); +} + +export function bigNumberToU64(value: BigNumber): StellarSdk.xdr.ScVal { + if (value.isNegative() || value.isGreaterThan(new BigNumber(2).pow(64).minus(1))) { + throw new Error('BigNumber is out of u64 range'); + } + + const b: bigint = BigInt(value.toFixed(0)); + const buf = bigintToBuf(b); + + if (buf.length > 8) { + throw new Error('BigNumber overflows u64'); + } + + // left-pad with zeros up to 8 bytes + let padded = Buffer.alloc(8); + buf.copy(padded, padded.length - buf.length); + + // Split padded into two parts for Uint64 + const hi = bigNumberFromBytes(false, ...padded.slice(0, 4)).toNumber(); + const lo = bigNumberFromBytes(false, ...padded.slice(4, 8)).toNumber(); + + return xdr.ScVal.scvU64(new xdr.Uint64([hi, lo])); +} + +export function bigNumberToU32(value: BigNumber): StellarSdk.xdr.ScVal { + if (value.isNegative() || value.isGreaterThan(new BigNumber(2).pow(32).minus(1))) { + throw new Error('BigNumber is out of u32 range'); + } + + const b: bigint = BigInt(value.toFixed(0)); + const buf = bigintToBuf(b); + + if (buf.length > 4) { + throw new Error('BigNumber overflows u32'); + } + + let padded = Buffer.alloc(4); + buf.copy(padded, padded.length - buf.length); + + const num = bigNumberFromBytes(false, ...padded).toNumber(); + + return xdr.ScVal.scvU32(num); +} diff --git a/src/helpers/xdr/bigint-encoder.ts b/src/helpers/xdr/bigint-encoder.ts new file mode 100644 index 0000000..2c5ebfc --- /dev/null +++ b/src/helpers/xdr/bigint-encoder.ts @@ -0,0 +1,77 @@ +// Ported from https://github.com/stellar/js-xdr/pull/96 +// Can remove this and use them through stellar base once it is merged + +/* eslint-disable */ +export function encodeBigIntFromBits(parts: any[], size: number, unsigned: boolean) { + let result = BigInt(0); + // check arguments length + if (parts.length && parts[0] instanceof Array) { + parts = parts[0]; + } + const total = parts.length; + if (total === 1) { + try { + result = BigInt(parts[0]); + if (!unsigned) { + result = BigInt.asIntN(size, result); + } + } catch (e) { + throw new TypeError(`Invalid integer value: ${parts[0]}`); + } + } else { + const sliceSize = size / total; + if (sliceSize !== 32 && sliceSize !== 64 && sliceSize !== 128) + throw new TypeError('Invalid number of arguments'); + // combine parts + for (let i = 0; i < total; i++) { + let part = BigInt.asUintN(sliceSize, BigInt(parts[i].valueOf())); + if (i > 0) { + // shift if needed + part <<= BigInt(i * sliceSize); + } + result |= part; + } + if (!unsigned) { + // clamp value to the requested size + result = BigInt.asIntN(size, result); + } + } + // check type + if (typeof result === 'bigint') { + // check boundaries + const [min, max] = calculateBigIntBoundaries(size, unsigned); + if (result >= min && result <= max) return result; + } + // failed to encode + throw new TypeError(`Invalid ${formatIntName(size, unsigned)} value`); +} + +export function sliceBigInt(value: BigInt, size: number, sliceSize: number) { + if (typeof value !== 'bigint') throw new TypeError('Invalid BigInt value'); + const total = size / sliceSize; + if (total === 1) return [value]; + if (sliceSize < 32 || sliceSize > 128 || (total !== 2 && total !== 4 && total !== 8)) + throw new TypeError('Invalid slice size'); + // prepare shift and mask + const shift = BigInt(sliceSize); + const mask = (BigInt(1) << shift) - BigInt(1); + // iterate shift and mask application + const result = new Array(total); + for (let i = 0; i < total; i++) { + if (i > 0) { + value >>= shift; + } + result[i] = BigInt.asIntN(sliceSize, value & mask); // clamp value + } + return result; +} + +export function formatIntName(precision: number, unsigned: boolean) { + return `${unsigned ? 'u' : 'i'}${precision}`; +} + +export function calculateBigIntBoundaries(size: number, unsigned: boolean) { + if (unsigned) return [BigInt(0), (BigInt(1) << BigInt(size)) - BigInt(1)]; + const boundary = BigInt(1) << BigInt(size - 1); + return [BigInt(0) - boundary, boundary - BigInt(1)]; +} diff --git a/src/helpers/xdr/errors.ts b/src/helpers/xdr/errors.ts new file mode 100644 index 0000000..261b578 --- /dev/null +++ b/src/helpers/xdr/errors.ts @@ -0,0 +1,23 @@ +export class XdrWriterError extends TypeError { + constructor(message: string) { + super(`XDR Write Error: ${message}`); + } +} + +export class XdrReaderError extends TypeError { + constructor(message: string) { + super(`XDR Read Error: ${message}`); + } +} + +export class XdrDefinitionError extends TypeError { + constructor(message: string) { + super(`XDR Type Definition Error: ${message}`); + } +} + +export class XdrNotImplementedDefinitionError extends XdrDefinitionError { + constructor() { + super(`method not implemented, it should be overloaded in the descendant class.`); + } +} diff --git a/src/helpers/xdr/i128.ts b/src/helpers/xdr/i128.ts new file mode 100644 index 0000000..35dcd35 --- /dev/null +++ b/src/helpers/xdr/i128.ts @@ -0,0 +1,17 @@ +import { LargeInt } from './large-int'; + +export class I128 extends LargeInt { + constructor(...args: any) { + super(args); + } + + get unsigned(): any { + return false; + } + + get size(): any { + return 128; + } +} + +I128.defineIntBoundaries(); diff --git a/src/helpers/xdr/index.ts b/src/helpers/xdr/index.ts new file mode 100644 index 0000000..ac24fa6 --- /dev/null +++ b/src/helpers/xdr/index.ts @@ -0,0 +1 @@ +export * from './i128'; diff --git a/src/helpers/xdr/large-int.ts b/src/helpers/xdr/large-int.ts new file mode 100644 index 0000000..e993c09 --- /dev/null +++ b/src/helpers/xdr/large-int.ts @@ -0,0 +1,116 @@ +// @ts-nocheck + +import { XdrPrimitiveType } from './xdr-type'; +import { calculateBigIntBoundaries, encodeBigIntFromBits, sliceBigInt } from './bigint-encoder'; +import { XdrNotImplementedDefinitionError, XdrWriterError } from './errors'; + +/* eslint-disable */ + +/* tslint:disable */ +export class LargeInt extends XdrPrimitiveType { + constructor(args) { + super(); + this._value = encodeBigIntFromBits(args, this.size, this.unsigned); + } + + /** + * Signed/unsigned representation + * @type {Boolean} + * @abstract + */ + get unsigned() { + throw new XdrNotImplementedDefinitionError(); + } + + /** + * Size of the integer in bits + * @type {Number} + * @abstract + */ + get size() { + throw new XdrNotImplementedDefinitionError(); + } + + /** + * Slice integer to parts with smaller bit size + * @param {32|64|128} sliceSize - Size of each part in bits + * @return {BigInt[]} + */ + slice(sliceSize) { + return sliceBigInt(this._value, this.size, sliceSize); + } + + toString() { + return this._value.toString(); + } + + toJSON() { + return { _value: this._value.toString() }; + } + + /** + * @inheritDoc + */ + static read(reader) { + const { size } = this.prototype; + if (size === 64) return new this(reader.readBigUInt64BE()); + return new this(...Array.from({ length: size / 64 }, () => reader.readBigUInt64BE()).reverse()); + } + + /** + * @inheritDoc + */ + static write(value, writer) { + if (value instanceof this) { + value = value._value; + } else if (typeof value !== 'bigint' || value > this.MAX_VALUE || value < this.MIN_VALUE) + throw new XdrWriterError(`${value} is not a ${this.name}`); + + const { unsigned, size } = this.prototype; + if (size === 64) { + if (unsigned) { + writer.writeBigUInt64BE(value); + } else { + writer.writeBigInt64BE(value); + } + } else { + for (const part of sliceBigInt(value, size, 64).reverse()) { + if (unsigned) { + writer.writeBigUInt64BE(part); + } else { + writer.writeBigInt64BE(part); + } + } + } + } + + /** + * @inheritDoc + */ + static isValid(value) { + return typeof value === 'bigint' || value instanceof this; + } + + /** + * Create instance from string + * @param {String} string - Numeric representation + * @return {LargeInt} + */ + static fromString(string) { + return new this(string); + } + + static MAX_VALUE = 0n; + + static MIN_VALUE = 0n; + + /** + * @internal + * @return {void} + */ + static defineIntBoundaries() { + const [min, max] = calculateBigIntBoundaries(this.prototype.size, this.prototype.unsigned); + this.MIN_VALUE = min; + this.MAX_VALUE = max; + } +} diff --git a/src/helpers/xdr/serialization/xdr-reader.ts b/src/helpers/xdr/serialization/xdr-reader.ts new file mode 100644 index 0000000..dbcc2ef --- /dev/null +++ b/src/helpers/xdr/serialization/xdr-reader.ts @@ -0,0 +1,86 @@ +import { XdrReaderError } from '../errors'; + +export class XdrReader { + private _buffer: any; + private _length: number; + private _index: number; + + constructor(source: any) { + let _source = source; + if (!Buffer.isBuffer(_source)) { + if (_source instanceof Array) { + _source = Buffer.from(source); + } else { + throw new XdrReaderError('source not specified'); + } + } + + this._buffer = _source; + this._length = _source.length; + this._index = 0; + } + + get eof() { + return this._index === this._length; + } + + advance(size: number) { + const from = this._index; + // advance cursor position + this._index += size; + // check buffer boundaries + if (this._length < this._index) + throw new XdrReaderError('attempt to read outside the boundary of the buffer'); + // check that padding is correct for Opaque and String + const padding = 4 - (size % 4 || 4); + if (padding > 0) { + // eslint-disable-next-line no-plusplus + for (let i = 0; i < padding; i++) + if (this._buffer[this._index + i] !== 0) + // all bytes in the padding should be zeros + throw new XdrReaderError('invalid padding'); + this._index += padding; + } + return from; + } + + rewind() { + this._index = 0; + } + + read(size: number) { + const from = this.advance(size); + return this._buffer.subarray(from, from + size); + } + + readInt32BE() { + return this._buffer.readInt32BE(this.advance(4)); + } + + readUInt32BE() { + return this._buffer.readUInt32BE(this.advance(4)); + } + + readBigInt64BE() { + return this._buffer.readBigInt64BE(this.advance(8)); + } + + readBigUInt64BE() { + return this._buffer.readBigUInt64BE(this.advance(8)); + } + + readFloatBE() { + return this._buffer.readFloatBE(this.advance(4)); + } + + readDoubleBE() { + return this._buffer.readDoubleBE(this.advance(8)); + } + + ensureInputConsumed() { + if (this._index !== this._length) + throw new XdrReaderError( + `invalid XDR contract typecast - source buffer not entirely consumed`, + ); + } +} diff --git a/src/helpers/xdr/serialization/xdr-writer.ts b/src/helpers/xdr/serialization/xdr-writer.ts new file mode 100644 index 0000000..34210e3 --- /dev/null +++ b/src/helpers/xdr/serialization/xdr-writer.ts @@ -0,0 +1,105 @@ +const BUFFER_CHUNK = 8192; // 8 KB chunk size increment + +export class XdrWriter { + private _buffer: any; + private _length: any; + + constructor(buffer: any) { + let _buffer = buffer; + if (typeof _buffer === 'number') { + _buffer = Buffer.allocUnsafe(_buffer); + } else if (!(buffer instanceof Buffer)) { + _buffer = Buffer.allocUnsafe(BUFFER_CHUNK); + } + this._buffer = _buffer; + this._length = _buffer.length; + } + + _index = 0; + + alloc(size: number) { + const from = this._index; + // advance cursor position + this._index += size; + // ensure sufficient buffer size + if (this._length < this._index) { + this.resize(this._index); + } + return from; + } + + resize(minRequiredSize: number) { + // calculate new length, align new buffer length by chunk size + const newLength = Math.ceil(minRequiredSize / BUFFER_CHUNK) * BUFFER_CHUNK; + // create new buffer and copy previous data + const newBuffer = Buffer.allocUnsafe(newLength); + this._buffer.copy(newBuffer, 0, 0, this._length); + // update references + this._buffer = newBuffer; + this._length = newLength; + } + + finalize() { + // clip underlying buffer to the actually written value + return this._buffer.subarray(0, this._index); + } + + toArray() { + return [...this.finalize()]; + } + + write(value: any, size: number) { + let _value = value; + if (typeof value === 'string') { + // serialize string directly to the output buffer + const offset = this.alloc(size); + this._buffer.write(_value, offset, 'utf8'); + } else { + // copy data to the output buffer + if (!(_value instanceof Buffer)) { + _value = Buffer.from(_value); + } + const offset = this.alloc(size); + _value.copy(this._buffer, offset, 0, size); + } + + // add padding for 4-byte XDR alignment + const padding = 4 - (size % 4 || 4); + if (padding > 0) { + const offset = this.alloc(padding); + this._buffer.fill(0, offset, this._index); + } + } + + writeInt32BE(value: string) { + const offset = this.alloc(4); + this._buffer.writeInt32BE(value, offset); + } + + writeUInt32BE(value: string) { + const offset = this.alloc(4); + this._buffer.writeUInt32BE(value, offset); + } + + writeBigInt64BE(value: string) { + const offset = this.alloc(8); + this._buffer.writeBigInt64BE(value, offset); + } + + writeBigUInt64BE(value: string) { + const offset = this.alloc(8); + this._buffer.writeBigUInt64BE(value, offset); + } + + writeFloatBE(value: string) { + const offset = this.alloc(4); + this._buffer.writeFloatBE(value, offset); + } + + writeDoubleBE(value: string) { + const offset = this.alloc(8); + this._buffer.writeDoubleBE(value, offset); + } + + static bufferChunkSize = BUFFER_CHUNK; +} diff --git a/src/helpers/xdr/xdr-type.ts b/src/helpers/xdr/xdr-type.ts new file mode 100644 index 0000000..38fe943 --- /dev/null +++ b/src/helpers/xdr/xdr-type.ts @@ -0,0 +1,170 @@ +// @ts-nocheck + +import { XdrReader } from './serialization/xdr-reader'; +import { XdrWriter } from './serialization/xdr-writer'; +import { XdrNotImplementedDefinitionError } from './errors'; + +/* eslint-disable */ +/* tslint:disable */ +class XdrType { + write: any; + + toXDR(format = 'raw') { + if (!this.write) return this.constructor.toXDR(this, format); + + const writer = new XdrWriter(); + this.write(this, writer); + return encodeResult(writer.finalize(), format); + } + + fromXDR(input, format = 'raw') { + if (!this.read) return this.constructor.fromXDR(input, format); + + const reader = new XdrReader(decodeInput(input, format)); + const result = this.read(reader); + reader.ensureInputConsumed(); + return result; + } + + /** + * Check whether input contains a valid XDR-encoded value + * @param {Buffer|String} input - XDR-encoded input data + * @param {XdrEncodingFormat} [format] - Encoding format (one of "raw", "hex", "base64") + * @return {Boolean} + */ + validateXDR(input, format = 'raw') { + try { + this.fromXDR(input, format); + return true; + } catch (e) { + return false; + } + } + + /** + * Encode value to XDR format + * @param {this} value - Value to serialize + * @param {XdrEncodingFormat} [format] - Encoding format (one of "raw", "hex", "base64") + * @return {Buffer} + */ + static toXDR(value, format = 'raw') { + const writer = new XdrWriter(); + this.write(value, writer); + return encodeResult(writer.finalize(), format); + } + + /** + * Decode XDR-encoded value + * @param {Buffer|String} input - XDR-encoded input data + * @param {XdrEncodingFormat} [format] - Encoding format (one of "raw", "hex", "base64") + * @return {this} + */ + static fromXDR(input, format = 'raw') { + const reader = new XdrReader(decodeInput(input, format)); + const result = this.read(reader); + reader.ensureInputConsumed(); + return result; + } + + /** + * Check whether input contains a valid XDR-encoded value + * @param {Buffer|String} input - XDR-encoded input data + * @param {XdrEncodingFormat} [format] - Encoding format (one of "raw", "hex", "base64") + * @return {Boolean} + */ + static validateXDR(input, format = 'raw') { + try { + this.fromXDR(input, format); + return true; + } catch (e) { + return false; + } + } +} + +export class XdrPrimitiveType extends XdrType { + /** + * Read value from the XDR-serialized input + * @param {XdrReader} reader - XdrReader instance + * @return {this} + * @abstract + */ + // eslint-disable-next-line no-unused-vars + static read(reader) { + throw new XdrNotImplementedDefinitionError(); + } + + /** + * Write XDR value to the buffer + * @param {this} value - Value to write + * @param {XdrWriter} writer - XdrWriter instance + * @return {void} + * @abstract + */ + // eslint-disable-next-line no-unused-vars + static write(value, writer) { + throw new XdrNotImplementedDefinitionError(); + } + + /** + * Check whether XDR primitive value is valid + * @param {this} value - Value to check + * @return {Boolean} + * @abstract + */ + // eslint-disable-next-line no-unused-vars + static isValid(value) { + return false; + } +} + +export class XdrCompositeType extends XdrType { + // Every descendant should implement two methods: read(reader) and write(value, writer) + + /** + * Check whether XDR primitive value is valid + * @param {this} value - Value to check + * @return {Boolean} + * @abstract + */ + // eslint-disable-next-line no-unused-vars + isValid(value) { + return false; + } +} + +class InvalidXdrEncodingFormatError extends TypeError { + constructor(format) { + super(`Invalid format ${format}, must be one of "raw", "hex", "base64"`); + } +} + +function encodeResult(buffer, format) { + switch (format) { + case 'raw': + return buffer; + case 'hex': + return buffer.toString('hex'); + case 'base64': + return buffer.toString('base64'); + default: + throw new InvalidXdrEncodingFormatError(format); + } +} + +function decodeInput(input, format) { + switch (format) { + case 'raw': + return input; + case 'hex': + return Buffer.from(input, 'hex'); + case 'base64': + return Buffer.from(input, 'base64'); + default: + throw new InvalidXdrEncodingFormatError(format); + } +} + +/** + * @typedef {'raw'|'hex'|'base64'} XdrEncodingFormat + */ diff --git a/src/hooks/useRouterAddress.tsx b/src/hooks/useRouterAddress.tsx new file mode 100644 index 0000000..9678497 --- /dev/null +++ b/src/hooks/useRouterAddress.tsx @@ -0,0 +1,25 @@ +import { SorobanContextType, useSorobanReact } from '@soroban-react/core'; +import { useEffect, useState } from 'react'; +import useSWRImmutable from 'swr/immutable'; +import { fetchRouter } from '../services/router'; + +export const useRouterAddress = () => { + const sorobanContext: SorobanContextType = useSorobanReact(); + + const { activeChain } = sorobanContext; + + const { data, error, isLoading, mutate } = useSWRImmutable( + ['router', activeChain], + ([key, activeChain]) => fetchRouter(activeChain?.id!) + ); + + const [router, setRouter] = useState(); + + useEffect(() => { + if (!data || !sorobanContext) return; + + setRouter(data.address); + }, [data, sorobanContext]); + + return { router, isLoading, isError: error, data }; +}; diff --git a/src/hooks/useRouterCallback.tsx b/src/hooks/useRouterCallback.tsx new file mode 100644 index 0000000..a307847 --- /dev/null +++ b/src/hooks/useRouterCallback.tsx @@ -0,0 +1,74 @@ +import { TxResponse, contractInvoke } from '@soroban-react/contracts'; + +import { useSorobanReact } from '@soroban-react/core'; +import * as StellarSdk from '@stellar/stellar-sdk'; +import { useCallback } from 'react'; +import { useSWRConfig } from 'swr'; +import { useRouterAddress } from './useRouterAddress'; + +export enum RouterMethod { + ADD_LIQUIDITY = 'add_liquidity', + REMOVE_LIQUIDITY = 'remove_liquidity', + SWAP_EXACT_IN = 'swap_exact_tokens_for_tokens', + SWAP_EXACT_OUT = 'swap_tokens_for_exact_tokens', + QUOTE = 'router_quote', + GET_AMOUNT_OUT = 'router_get_amount_out', + GET_AMOUNT_IN = 'router_get_amount_in', + GET_AMOUNTS_OUT = 'router_get_amounts_out', + GET_AMOUNTS_IN = 'router_get_amounts_in', +} + +// Returns a function that will execute a any method on RouterContract, if the parameters are all valid +// and the user has approved the slippage adjusted input amount for the trade + +const isObject = (val: any) => typeof val === 'object' && val !== null && !Array.isArray(val); + +//refetch balance, lptokens and reserves after router tx success +export const revalidateKeysCondition = (key: any) => { + const revalidateKeys = new Set([ + 'balance', + 'lp-tokens', + 'reserves', + 'trade', + 'subscribed-pairs', + 'currencyBalance', + 'horizon-account', + 'swap-network-fees', + ]); + + return Array.isArray(key) && key.some((k) => revalidateKeys.has(k)); +}; + +export function useRouterCallback() { + const sorobanContext = useSorobanReact(); + const { router } = useRouterAddress(); + const router_address = router!; + const { mutate } = useSWRConfig(); + + return useCallback( + async (method: RouterMethod, args?: StellarSdk.xdr.ScVal[], signAndSend?: boolean) => { + let result = (await contractInvoke({ + contractAddress: router_address as string, + method: method, + args: args, + sorobanContext, + signAndSend: signAndSend, + reconnectAfterTx: false, + })) as TxResponse; + + //If is only a simulation return the result + if (!signAndSend) return result; + + if ( + isObject(result) && + result?.status !== StellarSdk.SorobanRpc.Api.GetTransactionStatus.SUCCESS + ) + throw result; + + mutate((key: any) => revalidateKeysCondition(key), undefined, { revalidate: true }); + + return result; + }, + [router_address, sorobanContext, mutate] + ); +} diff --git a/src/hooks/useSwapCallback.tsx b/src/hooks/useSwapCallback.tsx new file mode 100644 index 0000000..8edc967 --- /dev/null +++ b/src/hooks/useSwapCallback.tsx @@ -0,0 +1,111 @@ +import * as StellarSdk from '@stellar/stellar-sdk'; +import BigNumber from 'bignumber.js'; +import { useWallet } from '../contexts/wallet'; // Adjust the import path if necessary +import { getCurrentTimePlusOneHour } from '../functions/getCurrentTimePlusOneHour'; +import { scValToJs } from '../helpers/convert'; +import { bigNumberToI128, bigNumberToU64 } from '../helpers/utils'; +import { InterfaceTrade, TradeType } from '../state/routing/types'; +import { RouterMethod, useRouterCallback } from './useRouterCallback'; + +// Returns a function that will execute a swap, if the parameters are all valid +// and the user has approved the slippage adjusted input amount for the trade + +interface GetSwapAmountsProps { + tradeType: TradeType; + inputAmount: string; + outputAmount: string; + allowedSlippage: number; +} + +export const getSwapAmounts = ({ + tradeType, + inputAmount, + outputAmount, + allowedSlippage = 0.5, +}: GetSwapAmountsProps) => { + const routerMethod = + tradeType == TradeType.EXACT_INPUT ? RouterMethod.SWAP_EXACT_IN : RouterMethod.SWAP_EXACT_OUT; + + const factorLess = BigNumber(100).minus(allowedSlippage).dividedBy(100); + const factorMore = BigNumber(100).plus(allowedSlippage).dividedBy(100); + + //amount_in , amount_out + const amount0 = + routerMethod === RouterMethod.SWAP_EXACT_IN ? BigNumber(inputAmount) : BigNumber(outputAmount); + + //amount_out_min , amount_in_max + const amount1 = + routerMethod === RouterMethod.SWAP_EXACT_IN + ? BigNumber(outputAmount).multipliedBy(factorLess).decimalPlaces(0) + : BigNumber(inputAmount).multipliedBy(factorMore).decimalPlaces(0); + + return { amount0, amount1, routerMethod }; +}; + +interface SuccessfulSwapResponse + extends StellarSdk.SorobanRpc.Api.GetSuccessfulTransactionResponse { + switchValues: string[]; +} + +export function useSwapCallback( + trade: InterfaceTrade | undefined // trade to execute, required +) { + const { connected, walletAddress } = useWallet(); // Use the wallet context + const routerCallback = useRouterCallback(); + const allowedSlippage = 1; + + const doSwap = async ( + simulation?: boolean + ): Promise => { + if (!trade) throw new Error('missing trade'); + if (!connected || !walletAddress) throw new Error('wallet must be connected to swap'); + if (!trade.tradeType) throw new Error('tradeType must be defined'); + + const { amount0, amount1, routerMethod } = getSwapAmounts({ + tradeType: trade.tradeType, + inputAmount: trade.inputAmount?.value as string, + outputAmount: trade.outputAmount?.value as string, + allowedSlippage: allowedSlippage, + }); + + const amount0ScVal = bigNumberToI128(amount0); + const amount1ScVal = bigNumberToI128(amount1); + + console.log('USING ROUTER'); + const path = trade.path?.map((address: string) => new StellarSdk.Address(address)); + + const pathScVal = StellarSdk.nativeToScVal(path); + + const args = [ + amount0ScVal, + amount1ScVal, + pathScVal, // path + new StellarSdk.Address(walletAddress).toScVal(), + bigNumberToU64(BigNumber(getCurrentTimePlusOneHour())), + ]; + + try { + const result = (await routerCallback( + routerMethod, + args, + !simulation + )) as StellarSdk.SorobanRpc.Api.GetTransactionResponse; + + //if it is a simulation should return the result + if (simulation) return result; + + if (result.status !== StellarSdk.SorobanRpc.Api.GetTransactionStatus.SUCCESS) throw result; + + const switchValues: string[] = scValToJs(result.returnValue!); + + const currencyA = switchValues?.[0]; + const currencyB = switchValues?.[switchValues?.length - 1]; + + return { ...result, switchValues }; + } catch (error) { + throw error; + } + }; + + return { doSwap, isLoading: trade?.isLoading }; +} diff --git a/src/interfaces/adminKeys.ts b/src/interfaces/adminKeys.ts new file mode 100644 index 0000000..01de01a --- /dev/null +++ b/src/interfaces/adminKeys.ts @@ -0,0 +1,10 @@ +export interface AdminKeyResponseType { + network: string; + admin_public: string; + admin_secret: string; +} + +export interface KeysType { + admin_public: string; + admin_secret: string; +} diff --git a/src/interfaces/currency.ts b/src/interfaces/currency.ts new file mode 100644 index 0000000..c78aff8 --- /dev/null +++ b/src/interfaces/currency.ts @@ -0,0 +1,6 @@ +import { TokenType } from 'interfaces'; + +export type CurrencyAmount = { + currency: TokenType; + value: string; +}; diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts new file mode 100644 index 0000000..c5b4697 --- /dev/null +++ b/src/interfaces/index.ts @@ -0,0 +1,3 @@ +export * from './tokens'; +export * from './adminKeys'; +export * from './currency'; diff --git a/src/interfaces/pairs.ts b/src/interfaces/pairs.ts new file mode 100644 index 0000000..39bb0e1 --- /dev/null +++ b/src/interfaces/pairs.ts @@ -0,0 +1,18 @@ +import BigNumber from 'bignumber.js'; +import { TokenType } from './tokens'; + +interface TokenAmount { + currency: TokenType | undefined; + balance: BigNumber; +} + +interface LiquidityToken { + token: TokenType | undefined; + userBalance: number | BigNumber; + totalSupply: any; +} + +export interface PairInfo { + liquidityToken: LiquidityToken; + tokenAmounts: TokenAmount[]; +} diff --git a/src/interfaces/tokens.ts b/src/interfaces/tokens.ts new file mode 100644 index 0000000..1e354ac --- /dev/null +++ b/src/interfaces/tokens.ts @@ -0,0 +1,23 @@ +export interface TokenType { + code: string; + issuer?: string; + contract: string; + name?: string; + org?: string; + domain?: string; + icon?: string; + decimals?: number; +} + +export interface tokensResponse { + network: string; + tokens: TokenType[]; +} + +export type TokenMapType = { + [key: string]: TokenType; +}; + +export type TokenBalancesMap = { + [tokenAddress: string]: { usdValue: number; balance: string }; +}; diff --git a/src/layouts/DefaultLayout.tsx b/src/layouts/DefaultLayout.tsx index d08dd89..9cd2d06 100644 --- a/src/layouts/DefaultLayout.tsx +++ b/src/layouts/DefaultLayout.tsx @@ -21,10 +21,13 @@ export default function DefaultLayout({ children }: { children: ReactNode }) { typeof poolId == 'string' && /^[0-9A-Z]{56}$/.test(poolId) ? poolId : undefined; const loadBlendData = useStore((state) => state.loadBlendData); + const rewardZone = useStore((state) => state.backstop?.config?.rewardZone ?? []); + + const isTestnet = process.env.NEXT_PUBLIC_PASSPHRASE === Networks.TESTNET; useEffect(() => { const update = async () => { - await loadBlendData(false, "CCG4HM7SML3CUKWO2WOXDR2HCH5EMIIYVNPFB2EMQPWI6KURL46XB54H", connected ? walletAddress : undefined); + await loadBlendData(false, undefined, connected ? walletAddress : undefined); }; update(); const refreshInterval = setInterval(async () => { @@ -33,6 +36,9 @@ export default function DefaultLayout({ children }: { children: ReactNode }) { return () => clearInterval(refreshInterval); }, [loadBlendData, connected, walletAddress]); + // get the last (oldest) pool in the reward zone + const faucet_pool = rewardZone.length > 0 ? rewardZone[rewardZone.length - 1] : undefined; + if (safePoolId && safePoolId !== lastPool) { setLastPool(safePoolId); } @@ -50,6 +56,11 @@ export default function DefaultLayout({ children }: { children: ReactNode }) { + {faucet_pool && isTestnet && ( + + + + )} {children} diff --git a/src/pages/borrow.tsx b/src/pages/borrow.tsx index 62a8ed3..bd35f80 100644 --- a/src/pages/borrow.tsx +++ b/src/pages/borrow.tsx @@ -1,18 +1,16 @@ -import OpenInNewIcon from '@mui/icons-material/OpenInNew'; -import { Box, Link, Typography, useTheme } from '@mui/material'; +import { Typography, useTheme } from '@mui/material'; import type { NextPage } from 'next'; import { useRouter } from 'next/router'; -import { BorrowAnvil } from '../components/borrow/BorrowAnvil'; import { FlameIcon } from '../components/common/FlameIcon'; -import { GoBackHeader } from '../components/common/GoBackHeader'; import { ReserveDropdown } from '../components/common/ReserveDropdown'; import { Row } from '../components/common/Row'; import { Section, SectionSize } from '../components/common/Section'; import { StackedText } from '../components/common/StackedText'; +import { LendAnvil } from '../components/lend/LendAnvil'; import { useWallet } from '../contexts/wallet'; import { useStore } from '../store/store'; import { getEmissionTextFromValue, toBalance, toPercentage } from '../utils/formatter'; -import { getEmissionsPerYearPerUnit, getTokenLinkFromReserve } from '../utils/token'; +import { getEmissionsPerYearPerUnit } from '../utils/token'; const Borrow: NextPage = () => { const theme = useTheme(); @@ -20,9 +18,10 @@ const Borrow: NextPage = () => { const router = useRouter(); const { poolId, assetId } = router.query; - const safePoolId = typeof poolId == 'string' && /^[0-9A-Z]{56}$/.test(poolId) ? poolId : ''; - const safeAssetId = typeof assetId == 'string' && /^[0-9A-Z]{56}$/.test(assetId) ? assetId : ''; - + const safePoolId = 'CBYCVLEHLOVGH6XYYOMXNXWC3AVSYSRUXK3MHWKVIQSDF7JQ2YNEF2FN'; + const safeAssetId = 'CBGO6D5Q3SIPG6QHN2MJ5LQQ6XH2SRPKEB6PLRPS3KWDDPLBMDETEZRK'; + const xlmAssetId = 'CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC'; + const usdcAssetId = 'CAQCFVLOBK5GIULPNZRGATJJMIZL5BSP7X5YJVMGCPTUEPFM4AVSRCJU'; const poolData = useStore((state) => state.pools.get(safePoolId)); const reserve = poolData?.reserves.get(safeAssetId); @@ -31,63 +30,8 @@ const Borrow: NextPage = () => { const totalSupplied = reserve?.estimates.supplied || 0; const availableToBorrow = totalSupplied * maxUtilFraction - (reserve?.estimates.borrowed || 0); - return ( + return poolData ? ( <> - - - - -
- -
-
- -
- - - - Available - - - {toBalance(availableToBorrow, reserve?.config.decimals)} - - - - - - {reserve?.tokenMetadata?.symbol ?? ''} - - - - - -
-
{
- +
+ +
+
+ + + + ) : ( + + {connected ? 'Loading...' : 'Please connect your wallet'} + ); }; diff --git a/src/pages/dashboard.tsx b/src/pages/dashboard.tsx index c120f6d..279555a 100644 --- a/src/pages/dashboard.tsx +++ b/src/pages/dashboard.tsx @@ -1,20 +1,13 @@ import { Box, Typography, useTheme } from '@mui/material'; import type { NextPage } from 'next'; import { useRouter } from 'next/router'; -import { BackstopPreviewBar } from '../components/backstop/BackstopPreviewBar'; -import { BorrowMarketList } from '../components/borrow/BorrowMarketList'; import { BorrowPositions } from '../components/borrow/BorrowPositions'; import { Divider } from '../components/common/Divider'; import { Row } from '../components/common/Row'; -import { Section, SectionSize } from '../components/common/Section'; -import { ToggleButton } from '../components/common/ToggleButton'; import { PositionOverview } from '../components/dashboard/PositionOverview'; -import { LendMarketList } from '../components/lend/LendMarketList'; import { LendPositions } from '../components/lend/LendPositions'; -import { PoolExploreBar } from '../components/pool/PoolExploreBar'; import { useSettings } from '../contexts'; import { useStore } from '../store/store'; -import { toBalance } from '../utils/formatter'; const Dashboard: NextPage = () => { const router = useRouter(); @@ -40,10 +33,6 @@ const Dashboard: NextPage = () => { return ( <> - - - - @@ -55,45 +44,6 @@ const Dashboard: NextPage = () => { - -
- - Supply - - - Borrow - -
-
- - {`Assets to ${showLend ? 'supply' : 'borrow'}`} - - - Market size: - - {`$${toBalance( - poolData?.estimates?.totalSupply ?? 0 - )}`} - - - - {showLend ? : } ); }; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index a6e7a24..62a8ed3 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,26 +1,140 @@ +import OpenInNewIcon from '@mui/icons-material/OpenInNew'; +import { Box, Link, Typography, useTheme } from '@mui/material'; import type { NextPage } from 'next'; -import { Divider } from '../components/common/Divider'; +import { useRouter } from 'next/router'; +import { BorrowAnvil } from '../components/borrow/BorrowAnvil'; +import { FlameIcon } from '../components/common/FlameIcon'; +import { GoBackHeader } from '../components/common/GoBackHeader'; +import { ReserveDropdown } from '../components/common/ReserveDropdown'; import { Row } from '../components/common/Row'; -import { SectionBase } from '../components/common/SectionBase'; -import { MarketCard } from '../components/markets/MarketCard'; +import { Section, SectionSize } from '../components/common/Section'; +import { StackedText } from '../components/common/StackedText'; +import { useWallet } from '../contexts/wallet'; import { useStore } from '../store/store'; +import { getEmissionTextFromValue, toBalance, toPercentage } from '../utils/formatter'; +import { getEmissionsPerYearPerUnit, getTokenLinkFromReserve } from '../utils/token'; -const Markets: NextPage = () => { - const pools = useStore((state) => state.pools); +const Borrow: NextPage = () => { + const theme = useTheme(); + const { connected, walletAddress } = useWallet(); + + const router = useRouter(); + const { poolId, assetId } = router.query; + const safePoolId = typeof poolId == 'string' && /^[0-9A-Z]{56}$/.test(poolId) ? poolId : ''; + const safeAssetId = typeof assetId == 'string' && /^[0-9A-Z]{56}$/.test(assetId) ? assetId : ''; + + const poolData = useStore((state) => state.pools.get(safePoolId)); + + const reserve = poolData?.reserves.get(safeAssetId); + //totalEstLiabilities / totalEstSupply , you you can just do something like canBorrow = totalSupply * max_util - totalLiabilities + const maxUtilFraction = (reserve?.config.max_util || 1) / 10 ** (reserve?.config.decimals || 7); + const totalSupplied = reserve?.estimates.supplied || 0; + const availableToBorrow = totalSupplied * maxUtilFraction - (reserve?.estimates.borrowed || 0); return ( <> - - Markets - + + + +
+ +
+
+ +
+ + + + Available + + + {toBalance(availableToBorrow, reserve?.config.decimals)} + + + + + + {reserve?.tokenMetadata?.symbol ?? ''} + + + + + +
+
+ +
+ + {toPercentage(reserve?.estimates.apy)}{' '} + + + } + sx={{ padding: '6px' }} + > +
+
+ +
+
+ +
+
+ + - - {Array.from(pools.keys()).map((poolId) => ( - - ))} ); }; -export default Markets; +export default Borrow; diff --git a/src/pages/swap.tsx b/src/pages/swap.tsx new file mode 100644 index 0000000..b5f2e52 --- /dev/null +++ b/src/pages/swap.tsx @@ -0,0 +1,256 @@ +import type { NextPage } from 'next'; + +// const Swap: NextPage = () => { +// const router = useRouter(); +// const { viewType } = useSettings(); +// const { connected, walletAddress, backstopClaim } = useWallet(); +// const loadBlendData = useStore((state) => state.loadBlendData); +// const { poolId } = router.query; +// const safePoolId = typeof poolId === 'string' && /^[0-9A-Z]{56}$/.test(poolId) ? poolId : ''; + +// const backstopData = useStore((state) => state.backstop); +// const userBackstopData = useStore((state) => state.backstopUserData); +// const userPoolEstimates = userBackstopData?.estimates.get(safePoolId); +// const userPoolBackstopData = userBackstopData?.balances.get(safePoolId); +// const [lpTokenEmissions, setLpTokenEmissions] = useState(); +// const [sellCoin, setSellCoin] = useState('XLM'); +// const [receiveCoin, setReceiveCoin] = useState('USDC'); +// const [sellAmount, setSellAmount] = useState(''); +// const [receiveAmount, setReceiveAmount] = useState(''); +// const [showConfirm, setShowConfirm] = useState(false); +// const [txError, setTxError] = useState(false); +// const [txErrorMessage, setTxErrorMessage] = useState(''); +// const [swapResult, setSwapResult] = useState(); +// const [isLoading, setIsLoading] = useState(false); + +// const conversionRate = 0.0899; // 1 XLM = 0.0899 USDC + +// const handleSellAmountChange = (amount: string) => { +// setSellAmount(amount); +// setReceiveAmount((parseFloat(amount) * conversionRate).toFixed(2)); +// }; + +// const handleReceiveAmountChange = (amount: string) => { +// setReceiveAmount(amount); +// setSellAmount((parseFloat(amount) / conversionRate).toFixed(2)); +// }; + +// const handleClaimEmissionsClick = async () => { +// if (connected && userBackstopData && userPoolEstimates?.emissions) { +// let claimArgs: BackstopClaimArgs = { +// from: walletAddress, +// pool_addresses: [safePoolId], +// to: walletAddress, +// }; +// setLpTokenEmissions(BigInt(0)); +// await backstopClaim(claimArgs, false); +// await loadBlendData(true, safePoolId, walletAddress); +// } +// }; + +// const trade: InterfaceTrade = { +// tradeType: TradeType.EXACT_INPUT, +// inputAmount: { value: sellAmount, currency: sellCoin }, +// outputAmount: { value: receiveAmount, currency: receiveCoin }, +// path: [sellCoin, receiveCoin], +// }; + +// const { doSwap } = useSwapCallback(trade); + +// const handleSwap = async () => { +// if (!connected) { +// setTxErrorMessage('Wallet not connected'); +// setTxError(true); +// return; +// } + +// setIsLoading(true); +// setTxError(false); +// setTxErrorMessage(''); + +// try { +// const result = await doSwap(); +// setSwapResult(result); +// setShowConfirm(true); +// } catch (error: any) { +// setTxErrorMessage(error.message || 'An unknown error occurred'); +// setTxError(true); +// } finally { +// setIsLoading(false); +// } +// }; + +// const handleConfirmDismiss = () => { +// setShowConfirm(false); +// setSwapResult(undefined); +// }; + +// const handleErrorDismiss = () => { +// setTxError(false); +// setTxErrorMessage(''); +// handleConfirmDismiss(); +// }; + +// return ( +// <> +// +// +// +// Swap +// +// +// + +// {lpTokenEmissions !== undefined && lpTokenEmissions > BigInt(0) && ( +// +//
+// +// Emissions to claim +// +// +// +// +//
+//
+// )} +//
+// +// +// +// +// +// +// +// +// +// +// +// +// +//
+ +// +// +// + +// +// +// +// Transaction Error +// +// +// {txErrorMessage} +// +// +// +// + +// +// +// +// Swap Successful +// +// +// {swapResult} +// +// +// +// + +// +// +// ); +// }; + +// export default Swap; + +const Swap: NextPage = () => { + return <>; +}; + +export default Swap; diff --git a/src/pages/termsofservice.tsx b/src/pages/termsofservice.tsx deleted file mode 100644 index 24a2b61..0000000 --- a/src/pages/termsofservice.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Box, Typography } from '@mui/material'; -import { NextPage } from 'next'; -import { Divider } from '../components/common/Divider'; -import { Row } from '../components/common/Row'; -import { TOS } from '../components/common/TOS'; - -const TermsOfService: NextPage = () => { - return ( - <> - <> - - Blend App Terms of Service - - - - - - - - ); -}; - -export default TermsOfService; diff --git a/src/services/axios.ts b/src/services/axios.ts new file mode 100644 index 0000000..7a07700 --- /dev/null +++ b/src/services/axios.ts @@ -0,0 +1,10 @@ +import axios from 'axios'; + +const axiosInstance = axios.create({ + baseURL: process.env.NEXT_PUBLIC_BACKEND_URL, + headers: { + 'Cache-Control': 'no-cache', + }, +}); + +export default axiosInstance; diff --git a/src/services/router.ts b/src/services/router.ts new file mode 100644 index 0000000..92e55d1 --- /dev/null +++ b/src/services/router.ts @@ -0,0 +1,10 @@ +import axios from './axios'; + +export const fetchRouter = async (network: string) => { + if (network == 'mainnet') + return { address: 'CBHNQTKJD76Q55TINIT3PPP3BKLIKIQEXPTQ32GUUU7I3CHBD5JECZLW' }; + + const { data } = await axios.get(`/api/${network}/router`); + + return data; +}; diff --git a/src/state/routing/types.ts b/src/state/routing/types.ts new file mode 100644 index 0000000..715735b --- /dev/null +++ b/src/state/routing/types.ts @@ -0,0 +1,301 @@ +import { Percent } from 'soroswap-router-sdk'; +import { DexDistribution } from '../../helpers/aggregator'; +import { CurrencyAmount } from '../../interfaces'; + +export enum TradeState { + LOADING, + INVALID, + STALE, + NO_ROUTE_FOUND, + VALID, +} + +export enum QuoteMethod { + ROUTING_API = 'ROUTING_API', + CLIENT_SIDE = 'CLIENT_SIDE', + CLIENT_SIDE_FALLBACK = 'CLIENT_SIDE_FALLBACK', // If client-side was used after the routing-api call failed. +} + +// // from https://github.com/Uniswap/routing-api/blob/main/lib/handlers/schema.ts + +// type TokenInRoute = Pick + +// export type V3PoolInRoute = { +// type: 'v3-pool' +// tokenIn: TokenInRoute +// tokenOut: TokenInRoute +// sqrtRatioX96: string +// liquidity: string +// tickCurrent: string +// fee: string +// amountIn?: string +// amountOut?: string + +// // not used in the interface +// address?: string +// } + +// type V2Reserve = { +// token: TokenInRoute +// quotient: string +// } + +// export type V2PoolInRoute = { +// type: 'v2-pool' +// tokenIn: TokenInRoute +// tokenOut: TokenInRoute +// reserve0: V2Reserve +// reserve1: V2Reserve +// amountIn?: string +// amountOut?: string + +// // not used in the interface +// // avoid returning it from the client-side smart-order-router +// address?: string +// } + +// export interface ClassicQuoteData { +// quoteId?: string +// requestId?: string +// blockNumber: string +// amount: string +// amountDecimals: string +// gasPriceWei: string +// gasUseEstimate: string +// gasUseEstimateQuote: string +// gasUseEstimateQuoteDecimals: string +// gasUseEstimateUSD: string +// methodParameters?: { calldata: string; value: string } +// quote: string +// quoteDecimals: string +// quoteGasAdjusted: string +// quoteGasAdjustedDecimals: string +// route: Array<(V3PoolInRoute | V2PoolInRoute)[]> +// routeString: string +// } + +// type URADutchOrderQuoteResponse = { +// routing: URAQuoteType.DUTCH_LIMIT +// quote: { +// auctionPeriodSecs: number +// deadlineBufferSecs: number +// orderInfo: DutchOrderInfoJSON +// quoteId?: string +// requestId?: string +// slippageTolerance: string +// } +// allQuotes: Array +// } +// type URAClassicQuoteResponse = { +// routing: URAQuoteType.CLASSIC +// quote: ClassicQuoteData +// allQuotes: Array +// } +// export type URAQuoteResponse = URAClassicQuoteResponse | URADutchOrderQuoteResponse + +// export function isClassicQuoteResponse(data: URAQuoteResponse): data is URAClassicQuoteResponse { +// return data.routing === URAQuoteType.CLASSIC +// } + +// export enum TradeFillType { +// Classic = 'classic', // Uniswap V1, V2, and V3 trades with on-chain routes +// UniswapX = 'uniswap_x', // off-chain trades, no routes +// } + +// export type ApproveInfo = { needsApprove: true; approveGasEstimateUSD: number } | { needsApprove: false } +// export type WrapInfo = { needsWrap: true; wrapGasEstimateUSD: number } | { needsWrap: false } + +// export class ClassicTrade extends Trade { +// public readonly fillType = TradeFillType.Classic +// approveInfo: ApproveInfo +// gasUseEstimateUSD?: number // gas estimate for swaps +// blockNumber: string | null | undefined +// isUniswapXBetter: boolean | undefined +// fromClientRouter: boolean | undefined +// requestId: string | undefined +// quoteMethod: QuoteMethod + +// constructor({ +// gasUseEstimateUSD, +// blockNumber, +// isUniswapXBetter, +// requestId, +// quoteMethod, +// approveInfo, +// ...routes +// }: { +// gasUseEstimateUSD?: number +// totalGasUseEstimateUSD?: number +// blockNumber?: string | null +// isUniswapXBetter?: boolean +// requestId?: string +// quoteMethod: QuoteMethod +// fromClientRouter?: boolean +// approveInfo: ApproveInfo +// v2Routes: { +// routev2: V2Route +// inputAmount: CurrencyAmount +// outputAmount: CurrencyAmount +// }[] +// v3Routes: { +// routev3: V3Route +// inputAmount: CurrencyAmount +// outputAmount: CurrencyAmount +// }[] +// tradeType: TradeType +// mixedRoutes?: { +// mixedRoute: MixedRouteSDK +// inputAmount: CurrencyAmount +// outputAmount: CurrencyAmount +// }[] +// }) { +// super(routes) +// this.blockNumber = blockNumber +// this.gasUseEstimateUSD = gasUseEstimateUSD +// this.isUniswapXBetter = isUniswapXBetter +// this.requestId = requestId +// this.quoteMethod = quoteMethod +// this.approveInfo = approveInfo +// } + +// // gas estimate for maybe approve + swap +// public get totalGasUseEstimateUSD(): number | undefined { +// if (this.approveInfo.needsApprove && this.gasUseEstimateUSD) { +// return this.approveInfo.approveGasEstimateUSD + this.gasUseEstimateUSD +// } + +// return this.gasUseEstimateUSD +// } +// } + +// export class DutchOrderTrade extends IDutchOrderTrade { +// public readonly fillType = TradeFillType.UniswapX +// quoteId?: string +// requestId?: string +// wrapInfo: WrapInfo +// approveInfo: ApproveInfo +// // The gas estimate of the reference classic trade, if there is one. +// classicGasUseEstimateUSD?: number +// auctionPeriodSecs: number +// deadlineBufferSecs: number +// slippageTolerance: Percent + +// constructor({ +// currencyIn, +// currenciesOut, +// orderInfo, +// tradeType, +// quoteId, +// requestId, +// wrapInfo, +// approveInfo, +// classicGasUseEstimateUSD, +// auctionPeriodSecs, +// deadlineBufferSecs, +// slippageTolerance, +// }: { +// currencyIn: Currency +// currenciesOut: Currency[] +// orderInfo: DutchOrderInfo +// tradeType: TradeType +// quoteId?: string +// requestId?: string +// approveInfo: ApproveInfo +// wrapInfo: WrapInfo +// classicGasUseEstimateUSD?: number +// auctionPeriodSecs: number +// deadlineBufferSecs: number +// slippageTolerance: Percent +// }) { +// super({ currencyIn, currenciesOut, orderInfo, tradeType }) +// this.quoteId = quoteId +// this.requestId = requestId +// this.approveInfo = approveInfo +// this.wrapInfo = wrapInfo +// this.classicGasUseEstimateUSD = classicGasUseEstimateUSD +// this.auctionPeriodSecs = auctionPeriodSecs +// this.deadlineBufferSecs = deadlineBufferSecs +// this.slippageTolerance = slippageTolerance +// } + +// public get totalGasUseEstimateUSD(): number { +// if (this.wrapInfo.needsWrap && this.approveInfo.needsApprove) { +// return this.wrapInfo.wrapGasEstimateUSD + this.approveInfo.approveGasEstimateUSD +// } + +// if (this.wrapInfo.needsWrap) return this.wrapInfo.wrapGasEstimateUSD +// if (this.approveInfo.needsApprove) return this.approveInfo.approveGasEstimateUSD + +// return 0 +// } +// } +export enum TradeType { + EXACT_INPUT = 'EXACT_INPUT', + EXACT_OUTPUT = 'EXACT_OUTPUT', +} +export type InterfaceTrade = { + inputAmount: CurrencyAmount | undefined; + outputAmount: CurrencyAmount | undefined; + tradeType: TradeType | undefined; + path: string[] | undefined; + distribution?: DexDistribution[] | undefined; + priceImpact?: Percent | undefined; + [x: string]: any; +}; + +export enum QuoteState { + SUCCESS = 'Success', + NOT_FOUND = 'Not found', +} + +// export type QuoteResult = +// | { +// state: QuoteState.NOT_FOUND +// data?: undefined +// } +// | { +// state: QuoteState.SUCCESS +// data: URAQuoteResponse +// } + +// export type TradeResult = +// | { +// state: QuoteState.NOT_FOUND +// trade?: undefined +// } +// | { +// state: QuoteState.SUCCESS +// trade: InterfaceTrade +// } + +// export enum PoolType { +// V2Pool = 'v2-pool', +// V3Pool = 'v3-pool', +// } + +// // swap router API special cases these strings to represent native currencies +// // all chains except for bnb chain and polygon +// // have "ETH" as native currency symbol +// export enum SwapRouterNativeAssets { +// MATIC = 'MATIC', +// BNB = 'BNB', +// AVAX = 'AVAX', +// ETH = 'ETH', +// } + +// export enum URAQuoteType { +// CLASSIC = 'CLASSIC', +// DUTCH_LIMIT = 'DUTCH_LIMIT', +// } + +// type ClassicAPIConfig = { +// protocols: Protocol[] +// } + +// type UniswapXConfig = { +// swapper?: string +// exclusivityOverrideBps?: number +// auctionPeriodSecs?: number +// } + +// export type RoutingConfig = (UniswapXConfig | ClassicAPIConfig)[] diff --git a/src/store/blendSlice.ts b/src/store/blendSlice.ts index c767a48..09a53f7 100644 --- a/src/store/blendSlice.ts +++ b/src/store/blendSlice.ts @@ -55,7 +55,7 @@ export const createBlendSlice: StateCreator = (se // all pools in the reward zone + the request pools are loaded on the backstop let horizonServer = get().horizonServer(); let pools = new Map(); - let pool = "CBYCVLEHLOVGH6XYYOMXNXWC3AVSYSRUXK3MHWKVIQSDF7JQ2YNEF2FN"; + let pool = 'CBYCVLEHLOVGH6XYYOMXNXWC3AVSYSRUXK3MHWKVIQSDF7JQ2YNEF2FN'; let assetStellarMetadata = new Map(); try { let pool_data = await Pool.load(network, pool, latest_ledger_close); @@ -65,8 +65,6 @@ export const createBlendSlice: StateCreator = (se assetStellarMetadata.set(reserve.assetId, metadata); } } - console.log('Loaded pool data for pool ' + pool); - console.log(pool_data); pools.set(pool, pool_data); } catch (e) { console.error('Unable to load pool data for pool ' + pool); diff --git a/src/store/userSlice.ts b/src/store/userSlice.ts index 6b21e5e..a674e9b 100644 --- a/src/store/userSlice.ts +++ b/src/store/userSlice.ts @@ -32,7 +32,7 @@ export const createUserSlice: StateCreator = (set, const networkPassphrase = network.passphrase; if (get().latestLedgerTimestamp == 0) { - await get().loadBlendData(true, "CBYCVLEHLOVGH6XYYOMXNXWC3AVSYSRUXK3MHWKVIQSDF7JQ2YNEF2FN"); + await get().loadBlendData(true, 'CBYCVLEHLOVGH6XYYOMXNXWC3AVSYSRUXK3MHWKVIQSDF7JQ2YNEF2FN'); } const backstop = get().backstop; diff --git a/src/theme.ts b/src/theme.ts index 7f99519..b712232 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -51,12 +51,12 @@ const theme: Theme = createTheme({ mode: 'dark', tonalOffset: 0, background: { - default: '#191B1F', - paper: '#212429E5', + default: '#F1F3F4', + paper: '#ffffff', }, primary: { - main: '#36B04A', - opaque: '#36B04A26', + main: '#185180', + opaque: '#b3e4ff', contrastText: 'white', }, secondary: { @@ -68,8 +68,8 @@ const theme: Theme = createTheme({ opaque: '#00C4EF26', }, borrow: { - main: '#FF8A00', - opaque: '#FF8A0026', + main: '#185180', + opaque: '#ffffff', }, backstop: { main: '#E16BFF', @@ -80,7 +80,7 @@ const theme: Theme = createTheme({ opaque: '#2775C930', }, accent: { - main: '#191B1F', + main: '#ffffff', opaque: '#191B1F', }, menu: { @@ -88,12 +88,12 @@ const theme: Theme = createTheme({ light: '#2E313893', }, text: { - primary: '#FFFFFF', + primary: '#185180', secondary: '#979797', }, warning: { - main: '#FFCB00', - opaque: '#FFCB0026', + main: '#fc1500', + opaque: '#fc150026', }, error: { main: '#FF3366', diff --git a/tsconfig.json b/tsconfig.json index 99710e8..73e4afb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,8 @@ "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", - "incremental": true + "incremental": true, + "downlevelIteration": true }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "exclude": ["node_modules"]