├── .browserslistrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github ├── setup-docker │ └── action.yml ├── setup-node │ └── action.yml ├── setup-rust │ └── action.yml └── workflows │ ├── deploy-contracts.yml │ ├── gh-pages.yml │ └── pr.yaml ├── .gitignore ├── .husky └── pre-commit ├── .lintstagedrc ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── cover.png ├── docker ├── .env ├── .env.test ├── Makefile ├── README.md ├── docker-compose.yml └── fuel-core │ ├── Dockerfile │ └── chainConfig.json ├── docs ├── CONTRIBUTING.md ├── GETTING_STARTED.md ├── LEGAL_DISCLAIMER.md └── assets │ ├── launch-button.png │ └── preview-pages.gif ├── package.json ├── packages ├── app │ ├── .env.example │ ├── .env.production │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── jest.config.ts │ ├── load.envs.ts │ ├── package.json │ ├── playwright.config.ts │ ├── playwright │ │ ├── App.spec.ts │ │ └── fixtures.ts │ ├── postcss.config.js │ ├── public │ │ ├── favicon.ico │ │ ├── fuel-logo-192x192.png │ │ ├── fuel-logo-512x512.png │ │ ├── icons │ │ │ ├── dai.svg │ │ │ ├── eth.svg │ │ │ ├── eth_dai.svg │ │ │ ├── other.svg │ │ │ └── sway.svg │ │ ├── illustrations │ │ │ ├── add-funds.png │ │ │ ├── create-wallet.png │ │ │ └── done.png │ │ ├── lp-bg.jpg │ │ ├── manifest.json │ │ └── robots.txt │ ├── scripts │ │ ├── contracts-init │ │ │ ├── index.ts │ │ │ ├── initializePool.ts │ │ │ ├── initializeTokenContract.ts │ │ │ ├── loadDockerEnv.ts │ │ │ └── tsconfig.json │ │ ├── gh-pages-preview.sh │ │ └── postinstall.sh │ ├── src │ │ ├── App.tsx │ │ ├── config.ts │ │ ├── main.tsx │ │ ├── routes.tsx │ │ ├── styles │ │ │ ├── base.css │ │ │ ├── components │ │ │ │ ├── accordion.css │ │ │ │ ├── actions-widget.css │ │ │ │ ├── button.css │ │ │ │ ├── card.css │ │ │ │ ├── coin-balance.css │ │ │ │ ├── coin-input.css │ │ │ │ ├── coin-selector.css │ │ │ │ ├── dialog.css │ │ │ │ ├── header.css │ │ │ │ ├── home-page.css │ │ │ │ ├── input.css │ │ │ │ ├── main-layout.css │ │ │ │ ├── popover.css │ │ │ │ ├── toast.css │ │ │ │ ├── tooltip.css │ │ │ │ └── welcome-page.css │ │ │ ├── index.css │ │ │ └── utilities.css │ │ ├── systems │ │ │ ├── Core │ │ │ │ ├── components │ │ │ │ │ ├── ActionsWidget.tsx │ │ │ │ │ ├── AnimatedPage.tsx │ │ │ │ │ ├── AssetItem.tsx │ │ │ │ │ ├── CoinBalance.tsx │ │ │ │ │ ├── CoinInput.tsx │ │ │ │ │ ├── CoinSelector.tsx │ │ │ │ │ ├── CoinsListDialog.tsx │ │ │ │ │ ├── ErrorBoundary.tsx │ │ │ │ │ ├── Header.tsx │ │ │ │ │ ├── MainLayout.tsx │ │ │ │ │ ├── MigrationWarning.tsx │ │ │ │ │ ├── NavigateBackButton.tsx │ │ │ │ │ ├── NetworkFeePreviewItem.tsx │ │ │ │ │ ├── PreviewTable.tsx │ │ │ │ │ ├── PrivateRoute.tsx │ │ │ │ │ ├── Providers.tsx │ │ │ │ │ ├── TokenIcon.tsx │ │ │ │ │ ├── WalletInfo.tsx │ │ │ │ │ ├── WalletWidget.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── context.tsx │ │ │ │ ├── hooks │ │ │ │ │ ├── __mocks__ │ │ │ │ │ │ ├── MockConnection.ts │ │ │ │ │ │ ├── useBalances.ts │ │ │ │ │ │ └── useWallet.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── useAssets.ts │ │ │ │ │ ├── useBalances.ts │ │ │ │ │ ├── useBreakpoint.ts │ │ │ │ │ ├── useCoinInput.ts │ │ │ │ │ ├── useCoinInputDisplayValue.ts │ │ │ │ │ ├── useCoinMetadata.ts │ │ │ │ │ ├── useContract.ts │ │ │ │ │ ├── useDebounce.tsx │ │ │ │ │ ├── useEthBalance.ts │ │ │ │ │ ├── useFuel.ts │ │ │ │ │ ├── usePubSub.ts │ │ │ │ │ ├── useSlippage.ts │ │ │ │ │ ├── useTokensMethods.ts │ │ │ │ │ ├── useTransactionCost.ts │ │ │ │ │ ├── useWallet.ts │ │ │ │ │ └── useWalletConnection.ts │ │ │ │ ├── index.tsx │ │ │ │ └── utils │ │ │ │ │ ├── chain.ts │ │ │ │ │ ├── coins.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── feedback.tsx │ │ │ │ │ ├── gas.ts │ │ │ │ │ ├── helpers.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── math.test.ts │ │ │ │ │ ├── math.ts │ │ │ │ │ ├── queryClient.tsx │ │ │ │ │ ├── relativeUrl.ts │ │ │ │ │ └── tokenList.ts │ │ │ ├── Faucet │ │ │ │ ├── components │ │ │ │ │ ├── FaucetApp.tsx │ │ │ │ │ ├── FaucetDialog.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── hooks │ │ │ │ │ ├── __mocks__ │ │ │ │ │ │ └── useFaucet.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── useCaptcha.ts │ │ │ │ │ ├── useFaucet.ts │ │ │ │ │ └── useFaucetDialog.tsx │ │ │ │ └── index.tsx │ │ │ ├── Home │ │ │ │ ├── components │ │ │ │ │ ├── Header.tsx │ │ │ │ │ └── HomeHero.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── pages │ │ │ │ │ ├── HomePage.tsx │ │ │ │ │ └── index.tsx │ │ │ │ └── routes.tsx │ │ │ ├── Mint │ │ │ │ ├── hooks │ │ │ │ │ ├── __mocks__ │ │ │ │ │ │ └── useMint.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── useMint.ts │ │ │ │ ├── index.tsx │ │ │ │ ├── pages │ │ │ │ │ ├── MintPage.tsx │ │ │ │ │ └── index.tsx │ │ │ │ └── routes.tsx │ │ │ ├── Pool │ │ │ │ ├── components │ │ │ │ │ ├── AddLiquidityButton.tsx │ │ │ │ │ ├── AddLiquidityPoolPrice.tsx │ │ │ │ │ ├── AddLiquidityPreview.tsx │ │ │ │ │ ├── NewPoolWarning.tsx │ │ │ │ │ ├── PoolCurrentPosition.tsx │ │ │ │ │ ├── PoolCurrentReserves.tsx │ │ │ │ │ ├── RemoveLiquidityPreview.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── hooks │ │ │ │ │ ├── __mocks__ │ │ │ │ │ │ ├── addLiquidity.ts │ │ │ │ │ │ └── useUserPosition.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── useAddLiquidity.tsx │ │ │ │ │ ├── usePoolInfo.ts │ │ │ │ │ ├── usePreviewRemoveLiquidity.ts │ │ │ │ │ └── useUserPositions.ts │ │ │ │ ├── index.tsx │ │ │ │ ├── machines │ │ │ │ │ └── addLiquidityMachine.ts │ │ │ │ ├── pages │ │ │ │ │ ├── AddLiquidity.test.tsx │ │ │ │ │ ├── AddLiquidity.tsx │ │ │ │ │ ├── PoolPage.tsx │ │ │ │ │ ├── Pools.test.tsx │ │ │ │ │ ├── Pools.tsx │ │ │ │ │ ├── RemoveLiquidity.test.tsx │ │ │ │ │ ├── RemoveLiquidity.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── portals │ │ │ │ │ └── AddLiquidityPortal.tsx │ │ │ │ ├── routes.tsx │ │ │ │ ├── selectors.ts │ │ │ │ ├── state.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils │ │ │ │ │ ├── helpers.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── queries.ts │ │ │ ├── Swap │ │ │ │ ├── components │ │ │ │ │ ├── PricePerToken.tsx │ │ │ │ │ ├── SwapPreview.tsx │ │ │ │ │ ├── SwapWidget.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── hooks │ │ │ │ │ ├── usePricePerToken.tsx │ │ │ │ │ ├── useSwap.tsx │ │ │ │ │ ├── useSwapButton.tsx │ │ │ │ │ ├── useSwapCoinInput.tsx │ │ │ │ │ ├── useSwapCoinSelector.tsx │ │ │ │ │ ├── useSwapGlobalState.tsx │ │ │ │ │ ├── useSwapMaxButton.tsx │ │ │ │ │ ├── useSwapPreview.tsx │ │ │ │ │ └── useSwapURLParams.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── machines │ │ │ │ │ └── swapMachine.ts │ │ │ │ ├── pages │ │ │ │ │ ├── SwapPage.test.tsx │ │ │ │ │ ├── SwapPage.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── routes.tsx │ │ │ │ ├── state.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils │ │ │ │ │ ├── helpers.test.ts │ │ │ │ │ ├── helpers.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── queries.ts │ │ │ ├── UI │ │ │ │ ├── components │ │ │ │ │ ├── Accordion.tsx │ │ │ │ │ ├── Button.tsx │ │ │ │ │ ├── ButtonGroup.tsx │ │ │ │ │ ├── Card.tsx │ │ │ │ │ ├── Dialog.tsx │ │ │ │ │ ├── Input.tsx │ │ │ │ │ ├── InvertButton.tsx │ │ │ │ │ ├── Link.tsx │ │ │ │ │ ├── Menu.tsx │ │ │ │ │ ├── NumberInput.tsx │ │ │ │ │ ├── Popover.tsx │ │ │ │ │ ├── SkeletonLoader.tsx │ │ │ │ │ ├── Spinner.tsx │ │ │ │ │ ├── Toaster.tsx │ │ │ │ │ ├── Tooltip.tsx │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ └── Welcome │ │ │ │ ├── components │ │ │ │ ├── AddAssets.tsx │ │ │ │ ├── AddFunds.tsx │ │ │ │ ├── MintAssets.tsx │ │ │ │ ├── StepsIndicator.tsx │ │ │ │ ├── WelcomeConnect.tsx │ │ │ │ ├── WelcomeImage.tsx │ │ │ │ ├── WelcomeNavItem.tsx │ │ │ │ ├── WelcomeSidebar.tsx │ │ │ │ ├── WelcomeSidebarBullet.tsx │ │ │ │ ├── WelcomeStep.tsx │ │ │ │ ├── WelcomeTerms.tsx │ │ │ │ └── index.tsx │ │ │ │ ├── hooks │ │ │ │ ├── index.tsx │ │ │ │ └── useWelcomeSteps.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── machines │ │ │ │ ├── index.ts │ │ │ │ ├── welcomeMachine.test.ts │ │ │ │ └── welcomeMachine.ts │ │ │ │ ├── pages │ │ │ │ ├── WelcomePage.tsx │ │ │ │ └── index.tsx │ │ │ │ └── routes.tsx │ │ ├── types │ │ │ ├── contracts │ │ │ │ ├── ExchangeContractAbi.d.ts │ │ │ │ ├── TokenContractAbi.d.ts │ │ │ │ ├── common.d.ts │ │ │ │ ├── factories │ │ │ │ │ ├── ExchangeContractAbi__factory.ts │ │ │ │ │ └── TokenContractAbi__factory.ts │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ └── vite-env.d.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── config │ ├── eslint.js │ ├── package.json │ ├── react-imports.js │ └── tsup.js ├── contracts │ ├── README.md │ ├── exchange_abi │ │ ├── .gitignore │ │ ├── Forc.toml │ │ └── src │ │ │ └── main.sw │ ├── exchange_contract │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ ├── Forc.toml │ │ ├── src │ │ │ └── main.sw │ │ └── tests │ │ │ └── harness.rs │ ├── swayswap_contract │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ ├── Forc.toml │ │ ├── src │ │ │ └── main.sw │ │ └── tests │ │ │ └── harness.rs │ ├── swayswap_helpers │ │ ├── Forc.toml │ │ └── src │ │ │ └── main.sw │ ├── token_abi │ │ ├── .gitignore │ │ ├── Forc.toml │ │ └── src │ │ │ └── main.sw │ └── token_contract │ │ ├── .gitignore │ │ ├── .rustc_info.json │ │ ├── Cargo.toml │ │ ├── Forc.toml │ │ ├── src │ │ └── main.sw │ │ └── tests │ │ └── harness.rs ├── scripts │ ├── README.md │ ├── package.json │ ├── src │ │ ├── actions │ │ │ ├── buildContract.ts │ │ │ ├── buildContracts.ts │ │ │ ├── buildTypes.ts │ │ │ ├── deployContractBinary.ts │ │ │ ├── deployContracts.ts │ │ │ ├── getWalletInstance.ts │ │ │ ├── prettifyContracts.ts │ │ │ ├── runAll.ts │ │ │ └── validateConfig.ts │ │ ├── bin │ │ │ ├── bin.ts │ │ │ └── index.ts │ │ ├── helpers │ │ │ ├── createConfig.ts │ │ │ ├── loader.ts │ │ │ └── replaceEventOnEnv.ts │ │ ├── index.ts │ │ ├── log.ts │ │ └── types.ts │ ├── tsconfig.json │ └── tsup.config.ts └── test-utils │ ├── config.ts │ ├── package.json │ ├── setup.ts │ ├── src │ ├── accessibility.ts │ ├── focus.ts │ ├── hooks.ts │ ├── index.ts │ ├── mocks │ │ ├── cookie.ts │ │ ├── image.ts │ │ ├── index.ts │ │ ├── localstorage.ts │ │ └── match-media.ts │ ├── press.ts │ ├── render.tsx │ ├── user-event.ts │ └── utils.ts │ ├── tsconfig.json │ └── tsup.config.js ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── scripts ├── ci-test.sh ├── create-test-env.sh ├── test-contracts.sh └── update-deps.sh ├── swayswap.config.ts ├── tsconfig.base.json ├── tsconfig.eslint.json ├── tsconfig.json ├── turbo.json └── vercel.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | # Supported browsers 2 | 3 | last 3 version 4 | > 5% 5 | not dead 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | tab_width = 2 5 | indent_size = 2 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | **/coverage/ 3 | *.js 4 | dist 5 | CHANGELOG.md 6 | packages/app/src/types 7 | contracts 8 | **/*.typegen.ts 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./packages/config/eslint'); 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Syntax highlighting of sway files as rust 2 | *.sw linguist-language=Rust 3 | -------------------------------------------------------------------------------- /.github/setup-docker/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Setup Docker' 2 | inputs: 3 | compose-version: 4 | description: 'Docker-compose version' 5 | default: 2.6.0 6 | password: 7 | description: 'Password' 8 | required: true 9 | runs: 10 | using: "composite" 11 | steps: 12 | - name: Setup Docker 13 | uses: docker/login-action@v2 14 | with: 15 | registry: ghcr.io 16 | username: ${{ github.repository_owner }} 17 | password: ${{ inputs.password }} 18 | 19 | # Make github action to use the latest version of 20 | # docker compose without it docker compose down 21 | # has issues with memory nil pointer 22 | # https://github.com/docker/compose/pull/9354 23 | - name: Install Compose 24 | uses: ndeloof/install-compose-action@v0.0.1 25 | with: 26 | version: v${{ inputs.compose-version }} 27 | 28 | - name: Docker info 29 | run: | 30 | docker info 31 | shell: 32 | bash -------------------------------------------------------------------------------- /.github/setup-node/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Setup Node.js env' 2 | 3 | inputs: 4 | node-version: 5 | description: 'Node version' 6 | default: 16 7 | pnpm-version: 8 | description: 'PNPM version' 9 | default: 7 10 | 11 | runs: 12 | using: "composite" 13 | steps: 14 | - name: Setup Node.js 15 | uses: actions/setup-node@v2 16 | with: 17 | node-version: ${{ inputs.node-version }} 18 | registry-url: "https://registry.npmjs.org" 19 | 20 | - name: Cache PNPM modules 21 | uses: actions/cache@v2 22 | with: 23 | path: ~/.pnpm-store 24 | key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} 25 | restore-keys: | 26 | ${{ runner.os }}- 27 | 28 | - uses: pnpm/action-setup@v2.2.2 29 | name: Install pnpm 30 | with: 31 | version: ${{ inputs.pnpm-version }} 32 | run_install: true 33 | -------------------------------------------------------------------------------- /.github/setup-rust/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Setup Rust env' 2 | 3 | inputs: 4 | rust-version: 5 | description: 'Rust version' 6 | default: 1.68.2 7 | forc-version: 8 | description: 'Forc version' 9 | default: 0.35.5 10 | fuel-core-version: 11 | description: 'Fuel core version' 12 | default: 0.17.4 13 | 14 | 15 | runs: 16 | using: "composite" 17 | steps: 18 | - name: Install toolchain 19 | uses: actions-rs/toolchain@v1 20 | with: 21 | profile: minimal 22 | toolchain: ${{ inputs.rust-version }} 23 | # selecting a toolchain either by action or manual `rustup` calls should happen 24 | # before the cache plugin, as it uses the current rustc version as its cache key 25 | override: true 26 | 27 | - uses: Swatinem/rust-cache@v1 28 | 29 | - name: Set git config 30 | run: | 31 | git config --global core.bigfilethreshold 100m 32 | shell: 33 | bash 34 | 35 | - uses: actions/checkout@v2 36 | - name: Install Fuel toolchain 37 | uses: FuelLabs/action-fuel-toolchain@master 38 | with: 39 | name: swayswap-toolchain 40 | components: forc@${{ inputs.forc-version }}, fuel-core@${{ inputs.fuel-core-version }} -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Pages 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | deploy: 9 | # Disable gh-pages deploy 10 | if: false 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: ./.github/setup-node 16 | 17 | - name: Build 18 | env: 19 | CI: false 20 | PUBLIC_URL: "/${{ github.event.repository.name }}" 21 | run: | 22 | pnpm build 23 | 24 | - name: Deploy 25 | uses: JamesIves/github-pages-deploy-action@v4.3.3 26 | with: 27 | clean: true 28 | branch: gh-pages 29 | folder: packages/app/dist 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | Forc.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # Remove macOS's .DS_Store files 14 | .DS_Store 15 | 16 | # Logs 17 | logs 18 | *.log 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | pnpm-debug.log* 23 | lerna-debug.log* 24 | 25 | # Editor directories and files 26 | .vscode/* 27 | !.vscode/extensions.json 28 | .idea 29 | .DS_Store 30 | *.suo 31 | *.ntvs* 32 | *.njsproj 33 | *.sln 34 | *.sw? 35 | 36 | # dependencies 37 | node_modules 38 | .pnp 39 | .pnp.js 40 | *.local 41 | 42 | # testing 43 | coverage 44 | /cypress 45 | 46 | # next.js 47 | .next/ 48 | out/ 49 | build 50 | dist 51 | dist-ssr 52 | 53 | # misc 54 | .DS_Store 55 | *.pem 56 | 57 | # local env files 58 | .env 59 | !docker/.env 60 | .env.local 61 | .env.development.local 62 | .env.test.local 63 | .env.production.local 64 | 65 | # Local db 66 | docker/db 67 | 68 | # turbo 69 | .turbo 70 | tsconfig.tsbuildinfo 71 | 72 | # storybook 73 | storybook-static 74 | 75 | # general 76 | contracts/token_contract/.rustc_info.json 77 | .vercel 78 | 79 | # Local actions to test CI using ACT 80 | act-actions 81 | 82 | # XState typegen 83 | *.typegen.ts 84 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm lint-staged 5 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "./**/*.{js,jsx,ts,tsx,html}": ["pnpm prettier:check", "pnpm lint:check"] 3 | } 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies=false 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.template 2 | dist 3 | .coverage_* 4 | coverage 5 | node_modules 6 | CHANGELOG.md 7 | .chglog/CHANGELOG.tpl.md 8 | storybook-static 9 | pnpm-lock.yaml 10 | yarn-lock.yaml 11 | .github 12 | packages/contracts 13 | .pnpm-store 14 | .env 15 | **/*.typegen.ts 16 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "overrides": [ 3 | { 4 | "files": ["*.js", "*.ts", ".tsx"], 5 | "options": { 6 | "printWidth": 100, 7 | "semi": true, 8 | "tabWidth": 2, 9 | "useTabs": false, 10 | "singleQuote": true, 11 | "bracketSpacing": true 12 | } 13 | }, 14 | { 15 | "files": ["*.js", "*.ts", "*.json"], 16 | "options": { 17 | "useTabs": false 18 | } 19 | }, 20 | { 21 | "files": "*.md", 22 | "options": { 23 | "useTabs": false 24 | } 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "dbaeumer.vscode-eslint", 5 | "FuelLabs.sway-vscode-plugin", 6 | "statelyai.stately-vscode" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "prettier.prettierPath": "./node_modules/prettier", 4 | "editor.formatOnSave": true, 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll.eslint": "explicit" 7 | }, 8 | "jest.jestCommandLine": "cd packages/app && pnpm jest", 9 | "[xml]": { 10 | "editor.defaultFormatter": "ms-vsliveshare.vsliveshare" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FuelLabs/swayswap/e168fd924f3f7363006221c6dfda1a0053a5a488/cover.png -------------------------------------------------------------------------------- /docker/.env: -------------------------------------------------------------------------------- 1 | ENVIRONMENT=development 2 | FUEL_CORE_PORT=4000 3 | FUEL_FAUCET_PORT=4040 4 | WALLET_SECRET=0xa449b1ffee0e2205fa924c6740cc48b3b473aa28587df6dab12abc245d1f5298 5 | DISPENSE_AMOUNT=500000000 6 | GAS_PRICE=1 7 | # Used by SwaySwap scripts 8 | PROVIDER_URL=http://localhost:4000/graphql 9 | SIZE_POOL="1000000.0" 10 | -------------------------------------------------------------------------------- /docker/.env.test: -------------------------------------------------------------------------------- 1 | ENVIRONMENT=test 2 | FUEL_CORE_PORT=4001 3 | FUEL_FAUCET_PORT=4041 4 | WALLET_SECRET=0xa449b1ffee0e2205fa924c6740cc48b3b473aa28587df6dab12abc245d1f5298 5 | DISPENSE_AMOUNT=500000000 6 | GAS_PRICE=1 7 | # Used by SwaySwap scripts 8 | PROVIDER_URL=http://localhost:4001/graphql 9 | SIZE_POOL="1000000.0" 10 | -------------------------------------------------------------------------------- /docker/Makefile: -------------------------------------------------------------------------------- 1 | services-run: 2 | docker compose --env-file ./.env -p swayswap_local up --build -d 3 | 4 | services-clean: 5 | docker compose -p swayswap_local down --rmi local -v --remove-orphans 6 | 7 | services-run-test: 8 | docker compose --env-file ./.env.test -p swayswap_test up --build -d 9 | 10 | services-clean-test: 11 | docker compose -p swayswap_test down --rmi local -v --remove-orphans 12 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | ## Fuel network environment with a Faucet API 2 | 3 | We enable developers to run locally a entire env with a 4 | `fuel-core` network and `faucet` api running together 5 | 6 | ### Environment variables 7 | 8 | | name | default | description | 9 | | ---------------- | ----------- | -------------------------------------------------------------------------------------------------------------------- | 10 | | ENVIRONMENT | development | This is used to append on volume and container name to enable multiple envs like test | 11 | | WALLET_SECRET | | Secret used on the faucet API, by default we use the same `privateKey` used on the genesis config `chainConfig.json` | 12 | | FUEL_CORE_PORT | 4000 | Fuel network PORT | 13 | | FUEL_FAUCET_PORT | 4040 | Faucet API PORT | 14 | | DISPENSE_AMOUNT | 50000000 | Faucet dispense amount | 15 | | GAS_PRICE | 1 | Set Fuel Core `--min-gas-price` | 16 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | swayswap-fuel-core: 5 | platform: linux/amd64 6 | container_name: "swayswap-fuel-core-${ENVIRONMENT:-development}" 7 | environment: 8 | MIN_GAS_PRICE: ${GAS_PRICE} 9 | # This is the private key of the consensus.PoA.signing_key in the chainConfig.json 10 | # this key is responsible for validating the transactions 11 | CONSENSUS_KEY: 0xa449b1ffee0e2205fa924c6740cc48b3b473aa28587df6dab12abc245d1f5298 12 | build: ./fuel-core 13 | ports: 14 | - "${FUEL_CORE_PORT:-4000}:4000" 15 | volumes: 16 | - swayswap-fuel-core-db:/mnt/db 17 | healthcheck: 18 | test: curl --fail http://localhost:4000/health || exit 1 19 | interval: 1s 20 | timeout: 5s 21 | retries: 10 22 | swayswap-faucet: 23 | platform: linux/amd64 24 | container_name: "swayswap-faucet-${ENVIRONMENT:-development}" 25 | environment: 26 | MIN_GAS_PRICE: ${GAS_PRICE} 27 | WALLET_SECRET_KEY: ${WALLET_SECRET} 28 | DISPENSE_AMOUNT: ${DISPENSE_AMOUNT} 29 | FUEL_NODE_URL: http://swayswap-fuel-core-${ENVIRONMENT:-development}:4000/graphql 30 | # Other configurations can be found at; 31 | # https://github.com/FuelLabs/faucet#configuration 32 | image: ghcr.io/fuellabs/faucet:v0.5.0 33 | ports: 34 | - "${FUEL_FAUCET_PORT:-4040}:3000" 35 | links: 36 | - swayswap-fuel-core 37 | depends_on: 38 | swayswap-fuel-core: 39 | condition: service_healthy 40 | 41 | volumes: 42 | swayswap-fuel-core-db: 43 | name: "fuel-core-db-${ENVIRONMENT:-development}" 44 | -------------------------------------------------------------------------------- /docker/fuel-core/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/fuellabs/fuel-core:v0.17.3 2 | 3 | ARG IP=0.0.0.0 4 | ARG PORT=4000 5 | ARG DB_PATH=./mnt/db/ 6 | 7 | ENV IP="${IP}" 8 | ENV PORT="${PORT}" 9 | ENV DB_PATH="${DB_PATH}" 10 | ENV MIN_GAS_PRICE="${MIN_GAS_PRICE}" 11 | ENV CONSENSUS_KEY="${CONSENSUS_KEY}" 12 | 13 | # Install curl to use on 14 | # healthcheck config 15 | RUN apt update 16 | RUN apt install curl -y 17 | 18 | WORKDIR /root/ 19 | 20 | COPY chainConfig.json . 21 | 22 | CMD exec ./fuel-core run \ 23 | --ip ${IP} \ 24 | --port ${PORT} \ 25 | --db-path ${DB_PATH} \ 26 | --min-gas-price ${MIN_GAS_PRICE} \ 27 | --vm-backtrace \ 28 | --poa-instant=true \ 29 | --consensus-key ${CONSENSUS_KEY} \ 30 | --utxo-validation \ 31 | --chain ./chainConfig.json 32 | 33 | EXPOSE ${PORT} 34 | -------------------------------------------------------------------------------- /docker/fuel-core/chainConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "chain_name": "local_testnet", 3 | "consensus": { 4 | "PoA": { 5 | "signing_key": "0x94ffcc53b892684acefaebc8a3d4a595e528a8cf664eeb3ef36f1020b0809d0d" 6 | } 7 | }, 8 | "parent_network": { 9 | "type": "LocalTest" 10 | }, 11 | "block_gas_limit": 5000000000, 12 | "transaction_parameters": { 13 | "contract_max_size": 16777216, 14 | "max_inputs": 255, 15 | "max_outputs": 255, 16 | "max_witnesses": 255, 17 | "max_gas_per_tx": 100000000, 18 | "max_script_length": 1048576, 19 | "max_script_data_length": 1048576, 20 | "max_static_contracts": 255, 21 | "max_storage_slots": 255, 22 | "max_predicate_length": 1048576, 23 | "max_predicate_data_length": 1048576, 24 | "gas_price_factor": 1000000000, 25 | "gas_per_byte": 4, 26 | "max_message_data_length": 1048576 27 | }, 28 | "wallet": { 29 | "address": "0x94ffcc53b892684acefaebc8a3d4a595e528a8cf664eeb3ef36f1020b0809d0d", 30 | "privateKey": "0xa449b1ffee0e2205fa924c6740cc48b3b473aa28587df6dab12abc245d1f5298" 31 | }, 32 | "initial_state": { 33 | "coins": [ 34 | { 35 | "owner": "0x94ffcc53b892684acefaebc8a3d4a595e528a8cf664eeb3ef36f1020b0809d0d", 36 | "amount": "0xFFFFFFFFFFFFFFFF", 37 | "asset_id": "0x0000000000000000000000000000000000000000000000000000000000000000" 38 | }, 39 | { 40 | "owner": "0x80d5e88c2b23ec2be6b2e76f3499a1a2755bb2773363785111a719513fb57b8e", 41 | "amount": "0x00000000FFFFFFFF", 42 | "asset_id": "0x0000000000000000000000000000000000000000000000000000000000000000" 43 | } 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /docs/LEGAL_DISCLAIMER.md: -------------------------------------------------------------------------------- 1 | # Legal Disclaimer 2 | 3 | Read all of this disclaimer before using SwaySwap. 4 | 5 | - This software is only for use by _developers_ who understand what the Fuel network is. 6 | - You are solely responsible for your use of this demo application ("SwaySwap"). 7 | - SwaySwap can't be used to exchange anything of value because it only connects to a "testnet", which only handles essentially fake ETH and other assets that are used for testing applications. 8 | - Do not send anything of value to any of the addresses or smart contracts involved with SwaySwap from any network. 9 | - Although it's difficult to imagine how this test application could cause you any loss or damages, if anything goes wrong it is completely at your risk, and no one will compensate you. 10 | -------------------------------------------------------------------------------- /docs/assets/launch-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FuelLabs/swayswap/e168fd924f3f7363006221c6dfda1a0053a5a488/docs/assets/launch-button.png -------------------------------------------------------------------------------- /docs/assets/preview-pages.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FuelLabs/swayswap/e168fd924f3f7363006221c6dfda1a0053a5a488/docs/assets/preview-pages.gif -------------------------------------------------------------------------------- /packages/app/.env.example: -------------------------------------------------------------------------------- 1 | VITE_FUEL_PROVIDER_URL=http://localhost:4000/graphql 2 | VITE_FUEL_FAUCET_URL=http://localhost:4040/dispense 3 | VITE_FAUCET_RECAPTCHA_KEY= 4 | VITE_CONTRACT_ID=0x0000000000000000000000000000000000000000000000000000000000000000 5 | VITE_TOKEN_ID1=0x0000000000000000000000000000000000000000000000000000000000000000 6 | VITE_TOKEN_ID2=0x0000000000000000000000000000000000000000000000000000000000000000 7 | -------------------------------------------------------------------------------- /packages/app/.env.production: -------------------------------------------------------------------------------- 1 | # 2 | # Production env file 3 | # This was added due to vercel issues on updating env variables 4 | # 5 | VITE_FUEL_PROVIDER_URL=https://beta-3.fuel.network/graphql 6 | VITE_FUEL_FAUCET_URL=https://faucet-beta-3.fuel.network/dispense 7 | VITE_FAUCET_RECAPTCHA_KEY=6Ld3cEwfAAAAAMd4QTs7aO85LyKGdgj0bFsdBfre 8 | VITE_CONTRACT_ID=0x004ff2b3b79a67c1d574fa84a52af46cf42dbc1ac0229d13ec7802460ede9118 9 | VITE_TOKEN_ID1=0x1bdeed96ee1e5eca0bd1d7eeeb51d03b0202c1faf764fec1b276ba27d5d61d89 10 | VITE_TOKEN_ID2=0x0d9be25f6bef5c945ce44db64b33da9235fbf1a9f690298698d899ad550abae1 11 | -------------------------------------------------------------------------------- /packages/app/.gitignore: -------------------------------------------------------------------------------- 1 | .env.test 2 | node_modules/ 3 | /test-results/ 4 | /playwright-report/ 5 | /playwright/.cache/ 6 | /playwright/fuel-wallet.zip 7 | /playwright/dist-crx 8 | /test-results/ 9 | /playwright-report/ 10 | /playwright/.cache/ 11 | -------------------------------------------------------------------------------- /packages/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | SwaySwap 17 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /packages/app/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import type { Config } from '@jest/types'; 3 | import baseConfig from '@swayswap/test-utils/config'; 4 | 5 | import './load.envs.ts'; 6 | import pkg from './package.json'; 7 | 8 | const config: Config.InitialOptions = { 9 | ...baseConfig, 10 | rootDir: __dirname, 11 | displayName: pkg.name, 12 | modulePathIgnorePatterns: ['playwright'], 13 | }; 14 | 15 | export default config; 16 | -------------------------------------------------------------------------------- /packages/app/load.envs.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv'; 2 | import { resolve } from 'path'; 3 | 4 | function getEnvName() { 5 | if (process.env.NODE_ENV === 'production') { 6 | return '.env.production'; 7 | } 8 | if (process.env.NODE_ENV === 'test') { 9 | return '.env.test'; 10 | } 11 | } 12 | 13 | // Load from more specific env file to generic -> 14 | [getEnvName(), '.env'].forEach((envFile) => { 15 | if (!envFile) return; 16 | config({ 17 | path: resolve(__dirname, envFile), 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/app/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import './load.envs'; 2 | import type { PlaywrightTestConfig } from '@playwright/test'; 3 | 4 | const { E2E_PORT = 9000 } = process.env; 5 | 6 | const config: PlaywrightTestConfig = { 7 | timeout: 180000, 8 | testDir: './playwright', 9 | /* Retry on CI only */ 10 | retries: process.env.CI ? 1 : 0, 11 | /* Opt out of parallel tests on CI. */ 12 | workers: process.env.CI ? 1 : undefined, 13 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 14 | reporter: 'html', 15 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 16 | use: { 17 | /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ 18 | actionTimeout: 15000, 19 | 20 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 21 | trace: 'on', 22 | permissions: ['clipboard-read', 'clipboard-write'], 23 | baseURL: `http://localhost:${E2E_PORT}/`, 24 | }, 25 | 26 | /* Run your local dev server before starting the tests */ 27 | webServer: { 28 | command: 'pnpm dev-test', 29 | port: Number(E2E_PORT), 30 | reuseExistingServer: false, 31 | }, 32 | }; 33 | 34 | export default config; 35 | -------------------------------------------------------------------------------- /packages/app/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-import': {}, 4 | 'tailwindcss/nesting': {}, 5 | tailwindcss: {}, 6 | autoprefixer: {}, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /packages/app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FuelLabs/swayswap/e168fd924f3f7363006221c6dfda1a0053a5a488/packages/app/public/favicon.ico -------------------------------------------------------------------------------- /packages/app/public/fuel-logo-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FuelLabs/swayswap/e168fd924f3f7363006221c6dfda1a0053a5a488/packages/app/public/fuel-logo-192x192.png -------------------------------------------------------------------------------- /packages/app/public/fuel-logo-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FuelLabs/swayswap/e168fd924f3f7363006221c6dfda1a0053a5a488/packages/app/public/fuel-logo-512x512.png -------------------------------------------------------------------------------- /packages/app/public/icons/dai.svg: -------------------------------------------------------------------------------- 1 | dai -------------------------------------------------------------------------------- /packages/app/public/icons/eth.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/app/public/icons/other.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/app/public/illustrations/add-funds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FuelLabs/swayswap/e168fd924f3f7363006221c6dfda1a0053a5a488/packages/app/public/illustrations/add-funds.png -------------------------------------------------------------------------------- /packages/app/public/illustrations/create-wallet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FuelLabs/swayswap/e168fd924f3f7363006221c6dfda1a0053a5a488/packages/app/public/illustrations/create-wallet.png -------------------------------------------------------------------------------- /packages/app/public/illustrations/done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FuelLabs/swayswap/e168fd924f3f7363006221c6dfda1a0053a5a488/packages/app/public/illustrations/done.png -------------------------------------------------------------------------------- /packages/app/public/lp-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FuelLabs/swayswap/e168fd924f3f7363006221c6dfda1a0053a5a488/packages/app/public/lp-bg.jpg -------------------------------------------------------------------------------- /packages/app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Fuels Wallet", 3 | "name": "Fuels Wallet App", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "fuel-logo-192x192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "fuel-logo-512x512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /packages/app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /packages/app/scripts/contracts-init/initializeTokenContract.ts: -------------------------------------------------------------------------------- 1 | import type { BigNumberish } from 'fuels'; 2 | import { bn } from 'fuels'; 3 | 4 | import type { TokenContractAbi } from '../../src/types/contracts'; 5 | 6 | export async function initializeTokenContract( 7 | tokenContract: TokenContractAbi, 8 | overrides: { gasPrice: BigNumberish }, 9 | mintAmount: string 10 | ) { 11 | const address = { 12 | value: tokenContract.account!.address.toB256(), 13 | }; 14 | 15 | try { 16 | process.stdout.write('Initialize Token Contract\n'); 17 | await tokenContract.functions 18 | .initialize(bn.parseUnits(mintAmount), address) 19 | .txParams(overrides) 20 | .call(); 21 | } catch (err) { 22 | process.stdout.write(`Token Contract already initialized\n${err}`); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/app/scripts/contracts-init/loadDockerEnv.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | 3 | const { NODE_ENV } = process.env; 4 | 5 | dotenv.config({ 6 | path: `../../docker/.env${NODE_ENV ? `.${NODE_ENV}` : ''}`, 7 | }); 8 | -------------------------------------------------------------------------------- /packages/app/scripts/contracts-init/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "target": "es6", 5 | "module": "commonjs", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "resolveJsonModule": true, 9 | "lib": ["es2019", "dom"], 10 | "declaration": true, 11 | "declarationMap": true, 12 | "baseUrl": "." 13 | }, 14 | "include": ["./index.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/app/scripts/gh-pages-preview.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export PUBLIC_URL="/swayswap/" 4 | export BUILD_PATH="dist"$PUBLIC_URL 5 | 6 | # Clean dist folder 7 | rm -rf dist 8 | 9 | # Build folder with BASE_URL 10 | pnpm exec tsc && pnpm exec vite build && 11 | 12 | # Copy to inside folder 13 | cp $BUILD_PATH/index.html dist/404.html 14 | 15 | # Run server and open on browser 16 | pnpm exec http-server dist -o $PUBLIC_URL -c-1 17 | -------------------------------------------------------------------------------- /packages/app/scripts/postinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ENV_FILE=.env 4 | if [ ! -f "$FILE" ]; then 5 | cp .env.example $ENV_FILE 6 | fi 7 | 8 | pnpm xstate:typegen 9 | -------------------------------------------------------------------------------- /packages/app/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { routes } from "./routes"; 2 | 3 | import { Providers } from "~/systems/Core"; 4 | 5 | export function App() { 6 | return {routes}; 7 | } 8 | -------------------------------------------------------------------------------- /packages/app/src/config.ts: -------------------------------------------------------------------------------- 1 | /** Link for the fuel network node */ 2 | export const FUEL_PROVIDER_URL = process.env.VITE_FUEL_PROVIDER_URL!; 3 | /** Link for the fuel faucet */ 4 | export const FUEL_FAUCET_URL = process.env.VITE_FUEL_FAUCET_URL!; 5 | /** Id (address) of the deployed swayswap contract */ 6 | export const CONTRACT_ID = process.env.VITE_CONTRACT_ID!; 7 | /** Id (address) of the deployed token 1 contract */ 8 | export const TOKEN_ID1 = process.env.VITE_TOKEN_ID1!; 9 | /** Id (address) of the deployed token 2 contract */ 10 | export const TOKEN_ID2 = process.env.VITE_TOKEN_ID2!; 11 | /** The site key is used to invoke recaptcha service on the website 12 | * to disable recaptcha this env should be empty or not declared */ 13 | export const RECAPTCHA_KEY = process.env.VITE_FAUCET_RECAPTCHA_KEY!; 14 | /** Decimal units */ 15 | export const DECIMAL_UNITS = 9; 16 | /** Amount of tokens to faucet */ 17 | export const MINT_AMOUNT = 2000; 18 | /** Slippage tolerance applied on swap and add liquidity */ 19 | export const SLIPPAGE_TOLERANCE = 0.005; 20 | /** Small network fee */ 21 | export const NETWORK_FEE = 1; 22 | /** Default deadline */ 23 | export const DEADLINE = 1000; 24 | /** Max presentation units to avoid show 9 decimals on screen */ 25 | export const FIXED_UNITS = 3; 26 | /** Min gas price required from the fuel-core */ 27 | export const GAS_PRICE = 1; 28 | /** Base block explorer url */ 29 | export const BLOCK_EXPLORER_URL = 'https://fuellabs.github.io/block-explorer-v2'; 30 | /** Is production env */ 31 | export const IS_DEVELOPMENT = process.env.NODE_ENV !== 'production'; 32 | -------------------------------------------------------------------------------- /packages/app/src/main.tsx: -------------------------------------------------------------------------------- 1 | import "@fontsource/inter/variable.css"; 2 | import "@fontsource/raleway/variable.css"; 3 | import "@fuel-ui/css"; 4 | 5 | import "./styles/index.css"; 6 | 7 | // import { inspect } from "@xstate/inspect"; 8 | import { createRoot } from "react-dom/client"; 9 | import { BrowserRouter } from "react-router-dom"; 10 | 11 | import { App } from "./App"; 12 | 13 | // inspect({ 14 | // iframe: false, 15 | // }); 16 | 17 | const { PUBLIC_URL } = process.env; 18 | createRoot(document.getElementById("root")!).render( 19 | 20 | 21 | 22 | ); 23 | -------------------------------------------------------------------------------- /packages/app/src/routes.tsx: -------------------------------------------------------------------------------- 1 | import { Routes, Route, Navigate } from "react-router-dom"; 2 | 3 | import { homeRoutes } from "./systems/Home"; 4 | 5 | import { mintRoutes } from "~/systems/Mint"; 6 | import { poolRoutes } from "~/systems/Pool"; 7 | import { swapRoutes } from "~/systems/Swap"; 8 | import { welcomeRoutes } from "~/systems/Welcome"; 9 | import { Pages } from "~/types"; 10 | 11 | export const routes = ( 12 | 13 | 14 | } /> 15 | {homeRoutes} 16 | {welcomeRoutes} 17 | {swapRoutes} 18 | {mintRoutes} 19 | {poolRoutes} 20 | 21 | 22 | ); 23 | -------------------------------------------------------------------------------- /packages/app/src/styles/components/accordion.css: -------------------------------------------------------------------------------- 1 | @layer components { 2 | @keyframes slide-down { 3 | from { 4 | height: 0; 5 | } 6 | to { 7 | height: var(--radix-accordion-content-height); 8 | } 9 | } 10 | 11 | @keyframes slide-up { 12 | from { 13 | height: var(--radix-accordion-content-height); 14 | } 15 | to { 16 | height: 0; 17 | } 18 | } 19 | 20 | .accordion--root { 21 | @apply max-w-[100%]; 22 | } 23 | 24 | .accordion--item { 25 | @apply overflow-hidden mt-[1px]; 26 | 27 | &:first-child { 28 | @apply mt-0; 29 | } 30 | &:focus-within { 31 | @apply relative z-10; 32 | } 33 | 34 | & ~ & { 35 | @apply mt-3; 36 | } 37 | } 38 | 39 | .accordion--header { 40 | all: unset; 41 | @apply flex border-b-2 border-b-gray-700; 42 | } 43 | 44 | .accordion--trigger { 45 | all: unset; 46 | @apply flex-1 flex items-center justify-between; 47 | @apply text-gray-200; 48 | 49 | &:hover { 50 | @apply cursor-pointer; 51 | } 52 | } 53 | 54 | .accordion--icon { 55 | transition: transform 300ms cubic-bezier(0.87, 0, 0.13, 1); 56 | 57 | [data-state="open"] & { 58 | transform: rotate(180deg); 59 | } 60 | } 61 | 62 | .accordion--content { 63 | @apply overflow-hidden text-gray-400 break-words w-full; 64 | 65 | &[data-state="open"] { 66 | animation: slide-down 300ms cubic-bezier(0.87, 0, 0.13, 1) forwards; 67 | } 68 | &[data-state="close"] { 69 | animation: slide-up 300ms cubic-bezier(0.87, 0, 0.13, 1) forwards; 70 | } 71 | 72 | & > div { 73 | @apply mt-3; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /packages/app/src/styles/components/actions-widget.css: -------------------------------------------------------------------------------- 1 | @layer components { 2 | .actionsWidget { 3 | @apply flex gap-1 mt-6 self-start flex-1 items-end pb-0; 4 | 5 | @media (min-width: 834px) { 6 | @apply p-6 fixed bottom-0 left-0; 7 | } 8 | 9 | & a, 10 | & a:hover { 11 | @apply no-underline; 12 | } 13 | } 14 | 15 | .actionsWidget--btn { 16 | @apply px-4 bg-gray-800 rounded-full h-10; 17 | } 18 | 19 | .actionsWidget--shareBtn { 20 | @apply px-3 gap-0 transition-all ease-linear; 21 | 22 | & .content { 23 | @apply w-0 opacity-0; 24 | } 25 | 26 | &:hover { 27 | @apply px-4 gap-2; 28 | } 29 | 30 | &:hover .content { 31 | @apply w-[auto] opacity-100; 32 | } 33 | } 34 | 35 | .faucetDialog { 36 | @apply min-w-[auto]; 37 | 38 | & .card { 39 | @apply max-w-[346px] min-w-[auto]; 40 | } 41 | 42 | & .card--title { 43 | @apply text-gray-100; 44 | } 45 | } 46 | 47 | .faucetCaptcha { 48 | @apply mt-4 mx-6 flex h-[80px] items-center justify-center; 49 | } 50 | .faucetCaptcha--widget.is-hidden { 51 | @apply absolute top-0 left-0 hidden h-0 w-0 overflow-hidden; 52 | } 53 | .faucetCaptcha--loading { 54 | @apply py-4 flex items-center justify-center gap-3; 55 | } 56 | .faucetCaptcha--error { 57 | @apply relative mx-0 text-red-500; 58 | 59 | &::before { 60 | @apply absolute top-0 -left-5 block content-[""] w-[2px] h-full bg-red-900; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/app/src/styles/components/button.css: -------------------------------------------------------------------------------- 1 | @layer components { 2 | .button { 3 | @apply appearance-none transition-main grid grid-flow-col items-center rounded-xl gap-2; 4 | @apply border focus-ring btn-active cursor-pointer; 5 | 6 | &[aria-disabled="true"] { 7 | @apply cursor-default; 8 | } 9 | &.button--readonly { 10 | @apply cursor-default; 11 | } 12 | } 13 | .button--sm { 14 | @apply text-sm px-2 h-8; 15 | } 16 | .button--md { 17 | @apply text-base px-4 py-2; 18 | } 19 | .button--lg { 20 | @apply text-lg px-4 py-2; 21 | } 22 | .button--base { 23 | @apply text-gray-200 border-transparent; 24 | @apply hover:text-gray-100 hover:border-gray-600; 25 | @apply focus:text-primary-400 focus:border-primary-500; 26 | @apply active:text-primary-400 active:border-primary-500; 27 | 28 | &.button--readonly, 29 | &[aria-disabled="true"] { 30 | @apply border-transparent text-gray-400; 31 | } 32 | } 33 | .button--ghost { 34 | @apply text-gray-400 border-transparent hover:bg-white/5; 35 | @apply focus:border-primary-500 active:border-gray-700; 36 | 37 | &.button--readonly, 38 | &[aria-disabled="true"] { 39 | @apply border-transparent bg-white/0; 40 | } 41 | } 42 | .button--primary { 43 | @apply bg-primary-500 hover:bg-primary-600; 44 | @apply text-primary-100 font-semibold border-transparent; 45 | @apply focus:ring-primary-300 focus:border-primary-300; 46 | @apply active:border-transparent; 47 | 48 | &.button--readonly, 49 | &[aria-disabled="true"] { 50 | @apply bg-primary-500/10 text-primary-500/50; 51 | } 52 | } 53 | .btn-active:not([aria-disabled="true"]) { 54 | @apply active:scale-[0.98]; 55 | } 56 | *[aria-pressed="true"], 57 | *[data-pressed="true"] { 58 | @apply scale-[0.9]; 59 | 60 | &[aria-disabled="true"] { 61 | @apply scale-100; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/app/src/styles/components/card.css: -------------------------------------------------------------------------------- 1 | @layer components { 2 | .card { 3 | @apply bg-gray-800 rounded-xl py-3; 4 | min-width: 100%; 5 | 6 | @media (min-width: 640px) { 7 | min-width: 450px; 8 | } 9 | } 10 | 11 | .card--divider { 12 | @apply border border-gray-700 border-b-0 my-3; 13 | } 14 | 15 | .card--title { 16 | @apply px-3 sm:px-5 flex justify-between text-xl; 17 | 18 | & > h2 { 19 | @apply flex items-center gap-2; 20 | } 21 | } 22 | 23 | .card--content { 24 | @apply px-3 sm:px-5 pb-2; 25 | } 26 | 27 | .card--title { 28 | @apply flex items-center; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/app/src/styles/components/coin-balance.css: -------------------------------------------------------------------------------- 1 | @layer components { 2 | .coinBalance--maxButton { 3 | @apply text-xs py-0 px-1 h-auto bg-primary-800/60 text-primary-500 hover:bg-primary-800; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/app/src/styles/components/coin-input.css: -------------------------------------------------------------------------------- 1 | @layer components { 2 | .coinInput { 3 | @apply bg-gray-700 rounded-xl p-2 border border-gray-700; 4 | } 5 | .coinInput--input { 6 | @apply w-[100px] flex-1 ml-2 h-10 bg-transparent placeholder:text-gray-300; 7 | @apply outline-none text-xl flex items-center; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/app/src/styles/components/coin-selector.css: -------------------------------------------------------------------------------- 1 | @layer components { 2 | .coinSelector { 3 | @apply h-10 px-2 rounded-xl gap-1 bg-gray-800; 4 | } 5 | .coinSelector:not([aria-disabled="true"]):not(.button--readonly) { 6 | @apply hover:text-gray-300 hover:border-gray-600; 7 | } 8 | .coinSelector[aria-disabled="true"] { 9 | @apply opacity-100; 10 | } 11 | .coinSelector--root { 12 | @apply flex flex-col items-end; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/app/src/styles/components/dialog.css: -------------------------------------------------------------------------------- 1 | @layer components { 2 | .dialog--overlay { 3 | @apply bg-black/70 fixed top-0 left-0 right-0 bottom-0 w-screen h-screen; 4 | @apply overflow-y-auto grid place-items-center; 5 | } 6 | .dialog { 7 | @apply relative z-10 bg-gray-800 text-gray-300 rounded-xl min-w-[300px] focus-ring; 8 | } 9 | .dialog--closeBtn { 10 | @apply h-auto absolute top-2 right-2 focus-ring p-1 rounded border-transparent; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/app/src/styles/components/home-page.css: -------------------------------------------------------------------------------- 1 | @layer components { 2 | .homePage { 3 | @apply p-6 flex flex-col h-[100vh] justify-between bg-cover; 4 | background-image: url("/lp-bg.jpg"); 5 | 6 | @media (max-width: 640px) { 7 | @apply mobile-fit-screen; 8 | height: auto; 9 | } 10 | } 11 | 12 | .homePage--header { 13 | @apply flex justify-between font-semibold; 14 | 15 | img { 16 | height: 46px; 17 | width: 46px; 18 | } 19 | } 20 | 21 | .homePage--menu { 22 | @apply flex items-center text-lg; 23 | 24 | > * { 25 | @apply ml-4 lg:ml-6; 26 | } 27 | 28 | & a { 29 | @apply text-gray-200; 30 | } 31 | } 32 | 33 | .homePage--hero { 34 | @apply p-4 lg:p-10 text-gray-50 max-w-[500px] lg:max-w-[700px]; 35 | 36 | h1 { 37 | @apply text-3xl sm:text-4xl font-bold font-display; 38 | } 39 | p { 40 | @apply text-gray-100 my-6; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/app/src/styles/components/input.css: -------------------------------------------------------------------------------- 1 | @layer components { 2 | .input { 3 | @apply appearance-none w-full rounded-xl bg-gray-700 px-4 py-2 outline-none; 4 | 5 | &:not(.input--readOnly) { 6 | @apply focus-ring text-gray-100; 7 | } 8 | } 9 | 10 | .input--readOnly { 11 | @apply opacity-60; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/app/src/styles/components/main-layout.css: -------------------------------------------------------------------------------- 1 | @layer components { 2 | .mainLayout { 3 | @apply flex flex-col; 4 | @apply h-screen text-gray-100 overflow-hidden; 5 | 6 | @media (max-width: 640px) { 7 | @apply mobile-fit-screen; 8 | height: auto; 9 | } 10 | } 11 | .mainLayout--wrapper { 12 | @apply flex flex-1 flex-col items-center p-4 pt-10 sm:pt-16 overflow-y-auto; 13 | } 14 | 15 | .mainLayout-errorContent { 16 | @apply w-[30rem] flex-1 rounded-xl p-4 m-2; 17 | } 18 | .mainLayout-confirmBtn { 19 | @apply bg-primary-500 my-2 rounded-xl py-2 px-8 text-l font-semibold; 20 | @apply items-center justify-center cursor-pointer border; 21 | @apply border-primary-500 hover:border-primary-600 mt-8; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/app/src/styles/components/popover.css: -------------------------------------------------------------------------------- 1 | @layer components { 2 | .popover { 3 | @apply relative rounded-xl bg-gray-900 text-gray-200 outline-none; 4 | } 5 | .popover--arrow { 6 | @apply absolute bottom-[-17px] left-[10px] text-gray-900; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/app/src/styles/components/toast.css: -------------------------------------------------------------------------------- 1 | @layer components { 2 | .toast { 3 | @apply px-4 py-3 flex items-center gap-2 bg-gray-800 text-gray-50; 4 | @apply rounded-xl; 5 | 6 | & div[role="status"] { 7 | display: -webkit-box; 8 | max-width: 200px; 9 | -webkit-line-clamp: 6; 10 | -webkit-box-orient: vertical; 11 | overflow: hidden; 12 | } 13 | 14 | a { 15 | @apply text-primary-300 hover:underline; 16 | } 17 | } 18 | .toast--close_btn { 19 | @apply transition-all p-1 h-auto hover:opacity-100 focus-ring rounded; 20 | @apply border-transparent self-start; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/app/src/styles/components/tooltip.css: -------------------------------------------------------------------------------- 1 | @layer components { 2 | .tooltip { 3 | @apply bg-gray-900 text-gray-900 text-xs text-center leading-relaxed rounded-xl py-2 px-3; 4 | } 5 | 6 | .tooltip--content { 7 | @apply text-gray-300; 8 | } 9 | 10 | .tooltip--arrow { 11 | @apply fill-current; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/app/src/styles/index.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss/base"; 2 | @import "./base.css"; 3 | 4 | @import "tailwindcss/components"; 5 | @import "./components/accordion.css"; 6 | @import "./components/button.css"; 7 | @import "./components/card.css"; 8 | @import "./components/dialog.css"; 9 | @import "./components/input.css"; 10 | @import "./components/popover.css"; 11 | @import "./components/tooltip.css"; 12 | @import "./components/toast.css"; 13 | 14 | @import "./components/header.css"; 15 | @import "./components/coin-input.css"; 16 | @import "./components/coin-selector.css"; 17 | @import "./components/main-layout.css"; 18 | @import "./components/welcome-page.css"; 19 | @import "./components/home-page.css"; 20 | @import "./components/actions-widget.css"; 21 | 22 | @import "tailwindcss/utilities"; 23 | @import "./utilities.css"; 24 | -------------------------------------------------------------------------------- /packages/app/src/styles/utilities.css: -------------------------------------------------------------------------------- 1 | @layer utilities { 2 | .focus-ring:not([aria-disabled="true"]) { 3 | @apply focus:outline-none focus:ring-inset focus:ring-1; 4 | @apply focus:ring-primary-500 active:ring-0 disabled:ring-0; 5 | } 6 | .transition-main { 7 | @apply transition ease-in duration-100; 8 | } 9 | .link { 10 | @apply text-primary-400 no-underline hover:underline rounded-xl; 11 | @apply focus:outline-none focus:underline; 12 | } 13 | .inner-shadow { 14 | box-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.2); 15 | } 16 | 17 | .mobile-fit-screen { 18 | position: absolute; 19 | top: 0; 20 | bottom: 0; 21 | left: 0; 22 | right: 0; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/components/ActionsWidget.tsx: -------------------------------------------------------------------------------- 1 | import { FaFaucet } from "react-icons/fa"; 2 | 3 | import { FaucetDialog, useFaucetDialog } from "~/systems/Faucet"; 4 | import { Button } from "~/systems/UI"; 5 | 6 | export function ActionsWidget() { 7 | const faucetDialog = useFaucetDialog(); 8 | 9 | return ( 10 |
11 | 20 | 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/components/AnimatedPage.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from "framer-motion"; 2 | import type { ReactNode } from "react"; 3 | 4 | const animations = { 5 | initial: { opacity: 0, x: -50 }, 6 | animate: { opacity: 1, x: 0 }, 7 | exit: { opacity: 0, x: 50 }, 8 | }; 9 | 10 | export function AnimatedPage({ children }: { children: ReactNode }) { 11 | return ( 12 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/components/AssetItem.tsx: -------------------------------------------------------------------------------- 1 | import type { BN } from "fuels"; 2 | 3 | import { useCoinInput } from "../hooks/useCoinInput"; 4 | 5 | import { CoinInput } from "./CoinInput"; 6 | import { CoinSelector } from "./CoinSelector"; 7 | 8 | import type { Coin } from "~/types"; 9 | 10 | type AssetAmount = Coin & { amount: BN }; 11 | type AssetItemProps = { 12 | coin: AssetAmount; 13 | }; 14 | 15 | export function AssetItem({ coin }: AssetItemProps) { 16 | const input = useCoinInput({ 17 | coin, 18 | amount: coin.amount, 19 | isReadOnly: true, 20 | }); 21 | 22 | return ( 23 | } 26 | /> 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/components/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import type { ErrorBoundaryProps as RootProps } from "react-error-boundary"; 3 | import { ErrorBoundary as Root } from "react-error-boundary"; 4 | 5 | type ErrorBoundaryProps = Pick & { children: ReactNode }; 6 | 7 | export function ErrorBoundary({ children, onReset }: ErrorBoundaryProps) { 8 | if (process.env.NODE_ENV === "test") return <>{children}; 9 | return ( 10 | ( 13 |
17 | Error 18 |
19 | {error.message} 20 |
21 |
22 | 28 |
29 | )} 30 | > 31 | {children} 32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/components/MainLayout.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import { Suspense, useContext } from "react"; 3 | import { useQueryErrorResetBoundary } from "react-query"; 4 | 5 | import { AppContext } from "../context"; 6 | import { useBreakpoint } from "../hooks"; 7 | 8 | import { ErrorBoundary } from "./ErrorBoundary"; 9 | import { Header } from "./Header"; 10 | 11 | import { SkeletonLoader } from "~/systems/UI"; 12 | 13 | type MainLayoutProps = { 14 | children?: ReactNode; 15 | }; 16 | 17 | function MainLayoutLoader() { 18 | const breakpoint = useBreakpoint(); 19 | const isSmall = breakpoint === "sm"; 20 | const width = isSmall ? 300 : 410; 21 | 22 | return ( 23 | 30 | 31 | 32 | 33 | 34 | 35 | ); 36 | } 37 | 38 | export function MainLayout({ children }: MainLayoutProps) { 39 | const { reset: resetReactQuery } = useQueryErrorResetBoundary(); 40 | const ctx = useContext(AppContext); 41 | 42 | return ( 43 | <> 44 |
45 | {!ctx?.justContent &&
} 46 |
47 | 48 | {process.env.NODE_ENV !== "test" ? ( 49 | }>{children} 50 | ) : ( 51 | children 52 | )} 53 | 54 |
55 |
56 | 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/components/MigrationWarning.tsx: -------------------------------------------------------------------------------- 1 | import { Alert } from "@fuel-ui/react"; 2 | import { useState } from "react"; 3 | 4 | import { LocalStorageKey } from "../utils"; 5 | 6 | const LOCALSTORAGE_MIGRATION_WARNING = `${LocalStorageKey}fuel--migration-warning`; 7 | 8 | const useMigrationWarning = () => { 9 | const [hide, setToHide] = useState( 10 | localStorage.getItem(LOCALSTORAGE_MIGRATION_WARNING) 11 | ); 12 | 13 | const handleHide = () => { 14 | localStorage.setItem(LOCALSTORAGE_MIGRATION_WARNING, "dismiss"); 15 | setToHide("dismiss"); 16 | }; 17 | 18 | return { 19 | onPress: handleHide, 20 | hide: !!hide, 21 | }; 22 | }; 23 | 24 | export const MigrationWarning = () => { 25 | const { hide, onPress } = useMigrationWarning(); 26 | 27 | if (hide) return null; 28 | 29 | return ( 30 | 31 | 32 | SwaySwap is now on Fuel testnet beta-3! This network does not contain 33 | previous transactions or balances. 34 | 35 | 36 | Dismiss 37 | 38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/components/NavigateBackButton.tsx: -------------------------------------------------------------------------------- 1 | import { BsArrowLeft } from "react-icons/bs"; 2 | import { useNavigate } from "react-router-dom"; 3 | 4 | export interface NavigateBackButtonProps { 5 | page?: string; 6 | } 7 | 8 | export function NavigateBackButton({ page }: NavigateBackButtonProps) { 9 | const navigate = useNavigate(); 10 | const backPage = page || "../"; 11 | 12 | return ( 13 | navigate(backPage)} 17 | /> 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/components/NetworkFeePreviewItem.tsx: -------------------------------------------------------------------------------- 1 | import type { BN } from "fuels"; 2 | import { format } from "fuels"; 3 | 4 | import { PreviewItem } from "./PreviewTable"; 5 | 6 | import type { Maybe } from "~/types"; 7 | 8 | export function NetworkFeePreviewItem({ 9 | networkFee, 10 | loading, 11 | }: { 12 | networkFee?: Maybe; 13 | loading?: boolean; 14 | }) { 15 | if (!networkFee || networkFee.lte(0)) return null; 16 | 17 | return ( 18 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/components/PreviewTable.tsx: -------------------------------------------------------------------------------- 1 | import cx from "classnames"; 2 | import type { ReactNode } from "react"; 3 | 4 | import { SkeletonLoader } from "~/systems/UI"; 5 | 6 | type TableItemProps = { 7 | title: ReactNode; 8 | value: ReactNode; 9 | className?: string; 10 | loading?: boolean; 11 | }; 12 | 13 | const PreviewValueLoading = () => ( 14 | 15 | 16 | 17 | ); 18 | 19 | export const PreviewItem = ({ 20 | title, 21 | value, 22 | className, 23 | loading, 24 | }: TableItemProps) => ( 25 |
26 |
{title}
27 | {loading ? :
{value}
} 28 |
29 | ); 30 | 31 | type PreviewTableProps = { 32 | title?: ReactNode; 33 | className?: string; 34 | children: ReactNode; 35 | }; 36 | 37 | export const PreviewTable = ({ 38 | title, 39 | children, 40 | className, 41 | ...props 42 | }: PreviewTableProps) => ( 43 |
44 | {title &&
{title}
} 45 |
46 | {children} 47 |
48 |
49 | ); 50 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/components/PrivateRoute.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import { useEffect } from "react"; 3 | import { useQuery } from "react-query"; 4 | import { Navigate, useNavigate } from "react-router-dom"; 5 | 6 | import { useFuel } from "../hooks/useFuel"; 7 | 8 | import { getAgreement } from "~/systems/Welcome"; 9 | import { Pages } from "~/types"; 10 | 11 | export function PrivateRoute({ children }: { children: ReactNode }) { 12 | const navigate = useNavigate(); 13 | const acceptAgreement = getAgreement(); 14 | const { fuel, error } = useFuel(); 15 | const { data: isConnected, isLoading } = useQuery( 16 | ["isConnected", fuel !== undefined], 17 | async () => { 18 | const isFuelConnected = await fuel?.isConnected(); 19 | return isFuelConnected; 20 | }, 21 | { 22 | enabled: Boolean(fuel), 23 | } 24 | ); 25 | 26 | function handleWalletConnectionError() { 27 | localStorage.clear(); 28 | navigate(Pages.welcome); 29 | } 30 | 31 | useEffect(() => { 32 | if (error !== "") { 33 | handleWalletConnectionError(); 34 | } 35 | }, [error]); 36 | 37 | useEffect(() => { 38 | const timeoutConnection = setInterval(async () => { 39 | const isFuelConnected = await fuel?.isConnected(); 40 | if (!isFuelConnected) { 41 | handleWalletConnectionError(); 42 | } 43 | }, 1000); 44 | return () => { 45 | clearTimeout(timeoutConnection); 46 | }; 47 | }, [isConnected, fuel]); 48 | 49 | if (acceptAgreement) { 50 | return <>{children}; 51 | } 52 | 53 | if (isLoading) { 54 | return
; 55 | } 56 | 57 | return ; 58 | } 59 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/components/Providers.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import { QueryClientProvider } from "react-query"; 3 | import { useLocation } from "react-router-dom"; 4 | 5 | import { AppContextProvider } from "../context"; 6 | import { queryClient } from "../utils"; 7 | 8 | import { Toaster, Dialog } from "~/systems/UI"; 9 | 10 | export const LocationDisplay = () => { 11 | const location = useLocation(); 12 | return
{location.pathname}
; 13 | }; 14 | 15 | type AppProps = { 16 | children?: ReactNode; 17 | }; 18 | 19 | // const IS_TEST = process.env.NODE_ENV === "test"; 20 | const IS_TEST = false; 21 | 22 | export function Providers({ children }: AppProps) { 23 | return ( 24 | <> 25 | 26 | 27 | 28 | 29 | {children} 30 | 31 | 32 | 33 | {IS_TEST && } 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/components/TokenIcon.tsx: -------------------------------------------------------------------------------- 1 | import cx from "classnames"; 2 | 3 | import type { Coin, Maybe } from "~/types"; 4 | 5 | const style = { 6 | icon: `inline-flex rounded-full border-2 border-transparent`, 7 | iconLast: `last:ml-[-10px] last:z-1 border-gray-800`, 8 | }; 9 | 10 | type TokenIconProps = { 11 | coinFrom?: Maybe; 12 | coinTo?: Maybe; 13 | size?: number; 14 | }; 15 | 16 | export function TokenIcon({ coinFrom, coinTo, size = 20 }: TokenIconProps) { 17 | if (!coinFrom) return null; 18 | // Force image dimensions for retro compatibility 19 | const dimensionStyle = { 20 | height: size, 21 | width: size, 22 | }; 23 | return ( 24 |
25 | 26 | {coinFrom.name} 33 | 34 | {coinTo && ( 35 | 36 | {coinTo.name} 43 | 44 | )} 45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/components/WalletInfo.tsx: -------------------------------------------------------------------------------- 1 | import { BiLinkExternal, BiWallet } from "react-icons/bi"; 2 | import { MdClose } from "react-icons/md"; 3 | 4 | import { useAssets, useWallet } from "../hooks"; 5 | import { getBlockExplorerLink } from "../utils/feedback"; 6 | 7 | import { AssetItem } from "./AssetItem"; 8 | 9 | import { Button, Card, Link, Spinner } from "~/systems/UI"; 10 | 11 | type WalletInfoProps = { 12 | onClose: () => void; 13 | }; 14 | 15 | export function WalletInfo({ onClose }: WalletInfoProps) { 16 | const { coins, isLoading } = useAssets(); 17 | const { wallet } = useWallet(); 18 | 19 | return ( 20 | 21 | 22 |
23 | 24 | Wallet 25 |
26 | 29 |
30 | {isLoading && ( 31 |
32 | 33 |
34 | )} 35 | {coins.map((coin) => ( 36 |
37 | 38 |
39 | ))} 40 |
41 | 46 | View on Fuel Explorer 47 | 48 |
49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/components/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./ActionsWidget"; 2 | export * from "./AnimatedPage"; 3 | export * from "./AssetItem"; 4 | export * from "./CoinBalance"; 5 | export * from "./CoinInput"; 6 | export * from "./CoinSelector"; 7 | export * from "./CoinsListDialog"; 8 | export * from "./ErrorBoundary"; 9 | export * from "./Header"; 10 | export * from "./MainLayout"; 11 | export * from "./NavigateBackButton"; 12 | export * from "./PreviewTable"; 13 | export * from "./PrivateRoute"; 14 | export * from "./Providers"; 15 | export * from "./TokenIcon"; 16 | export * from "./WalletInfo"; 17 | export * from "./WalletWidget"; 18 | export * from "./NetworkFeePreviewItem"; 19 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/context.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import React, { useContext } from "react"; 3 | 4 | import type { Maybe } from "~/types"; 5 | 6 | interface AppContextValue { 7 | justContent?: boolean; 8 | } 9 | 10 | export const AppContext = React.createContext>(null); 11 | 12 | export const useAppContext = () => useContext(AppContext)!; 13 | 14 | type ProviderProps = { 15 | children: ReactNode; 16 | justContent?: boolean; 17 | }; 18 | 19 | export const AppContextProvider = ({ 20 | justContent, 21 | children, 22 | }: ProviderProps) => { 23 | return ( 24 | 29 | {children} 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/hooks/__mocks__/useBalances.ts: -------------------------------------------------------------------------------- 1 | import type { CoinQuantity } from 'fuels'; 2 | import { bn } from 'fuels'; 3 | 4 | import { COIN_ETH } from '../../utils'; 5 | import * as useBalances from '../useBalances'; 6 | 7 | import { DECIMAL_UNITS } from '~/config'; 8 | 9 | const FAKE_BALANCE = [{ amount: bn.parseUnits('3', DECIMAL_UNITS), assetId: COIN_ETH }]; 10 | 11 | export function mockUseBalances(balances?: CoinQuantity[]) { 12 | const mock = { 13 | data: balances || FAKE_BALANCE, 14 | loading: false, 15 | refetch: async () => balances || FAKE_BALANCE, 16 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 17 | } as any; 18 | 19 | return jest.spyOn(useBalances, 'useBalances').mockImplementation(() => mock); 20 | } 21 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useAssets'; 2 | export * from './useBalances'; 3 | export * from './useBreakpoint'; 4 | export * from './useCoinInput'; 5 | export * from './useCoinMetadata'; 6 | export * from './useContract'; 7 | export * from './useDebounce'; 8 | export * from './useEthBalance'; 9 | export * from './usePubSub'; 10 | export * from './useSlippage'; 11 | export * from './useTokensMethods'; 12 | export * from './useTransactionCost'; 13 | export * from './useWallet'; 14 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/hooks/useAssets.ts: -------------------------------------------------------------------------------- 1 | import type { BN, CoinQuantity } from 'fuels'; 2 | import { bn } from 'fuels'; 3 | import { useQuery } from 'react-query'; 4 | 5 | import { TOKENS, ASSET_404 } from '../utils'; 6 | 7 | import { useWallet } from './useWallet'; 8 | 9 | import type { Coin } from '~/types'; 10 | 11 | type AssetAmount = Coin & { amount: BN }; 12 | const mergeCoinsWithMetadata = (coins: CoinQuantity[] = []): Array => 13 | coins.map((coin) => { 14 | const coinMetadata = TOKENS.find((c) => c.assetId === coin.assetId); 15 | return { 16 | // TODO: Create default Coin Metadata when token didn't have registered data 17 | // Another options could be querying from the contract 18 | // https://github.com/FuelLabs/swayswap-demo/issues/33 19 | name: coinMetadata?.name || ASSET_404.name, 20 | img: coinMetadata?.img || ASSET_404.img, 21 | pairOf: coinMetadata?.pairOf, 22 | assetId: coin.assetId, 23 | amount: bn(coin.amount || 0), 24 | }; 25 | }); 26 | 27 | export function useAssets() { 28 | const { wallet } = useWallet(); 29 | 30 | const { 31 | isLoading, 32 | data: balances, 33 | refetch, 34 | } = useQuery('AssetsPage-balances', () => wallet?.getBalances()); 35 | 36 | const coins = mergeCoinsWithMetadata(balances); 37 | return { coins, refetch, isLoading }; 38 | } 39 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/hooks/useBalances.ts: -------------------------------------------------------------------------------- 1 | import type { UseQueryOptions } from 'react-query'; 2 | import { useQuery } from 'react-query'; 3 | 4 | import { usePublisher } from './usePubSub'; 5 | import { useWallet } from './useWallet'; 6 | 7 | import { Queries, AppEvents } from '~/types'; 8 | 9 | export function useBalances(opts: UseQueryOptions = {}) { 10 | const { wallet } = useWallet(); 11 | const publisher = usePublisher(); 12 | 13 | const optss = useQuery( 14 | Queries.UserQueryBalances, 15 | async () => { 16 | if (!wallet) return []; 17 | const balances = await wallet.getBalances(); 18 | return balances; 19 | }, 20 | { 21 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 22 | ...(opts as any), 23 | onSuccess(data) { 24 | opts.onSuccess?.(data); 25 | publisher.emit(AppEvents.updatedBalances, data); 26 | }, 27 | initialData: [], 28 | enabled: Boolean(wallet), 29 | } 30 | ); 31 | 32 | return optss; 33 | } 34 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/hooks/useBreakpoint.ts: -------------------------------------------------------------------------------- 1 | import { createBreakpoint } from 'react-use'; 2 | 3 | export const useBreakpoint = createBreakpoint({ sm: 300, md: 768, lg: 1024 }); 4 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/hooks/useCoinInputDisplayValue.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from 'react'; 2 | 3 | import type { CoinInputProps } from './useCoinInput'; 4 | 5 | export function useCoinInputDisplayValue( 6 | initialValue: string, 7 | onChange: CoinInputProps['onChange'] 8 | ): [string, (e: React.ChangeEvent) => void] { 9 | const [value, setValue] = useState(initialValue); 10 | 11 | useEffect(() => { 12 | setValue(initialValue); 13 | }, [initialValue]); 14 | 15 | useEffect(() => { 16 | if (value !== initialValue) onChange?.(value); 17 | }, [value]); 18 | 19 | const valueSetter = useCallback( 20 | (e: React.ChangeEvent) => { 21 | const valueWithoutLeadingZeros = e.target.value.replace(/^0+\d/, (substring) => 22 | substring.replace(/^0+(?=[\d])/, '') 23 | ); 24 | 25 | setValue( 26 | valueWithoutLeadingZeros.startsWith('.') 27 | ? `0${valueWithoutLeadingZeros}` 28 | : valueWithoutLeadingZeros 29 | ); 30 | }, 31 | [setValue] 32 | ); 33 | 34 | return [value, valueSetter]; 35 | } 36 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/hooks/useCoinMetadata.ts: -------------------------------------------------------------------------------- 1 | import { TOKENS } from '../utils'; 2 | 3 | export interface UseCoinMetadata { 4 | symbol?: string; 5 | } 6 | 7 | export function useCoinMetadata({ symbol }: UseCoinMetadata) { 8 | return { 9 | coinMetaData: TOKENS.find((c) => c.symbol === symbol), 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/hooks/useContract.ts: -------------------------------------------------------------------------------- 1 | import { useWallet } from './useWallet'; 2 | 3 | import { CONTRACT_ID } from '~/config'; 4 | import { ExchangeContractAbi__factory } from '~/types/contracts'; 5 | 6 | export const useContract = () => { 7 | const { wallet } = useWallet(); 8 | return wallet && ExchangeContractAbi__factory.connect(CONTRACT_ID, wallet); 9 | }; 10 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/hooks/useDebounce.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export function useDebounce(value: T, delay: number = 300) { 4 | const [debouncedValue, setDebouncedValue] = useState(value); 5 | 6 | useEffect(() => { 7 | const handler = setTimeout(() => { 8 | setDebouncedValue(value); 9 | }, delay); 10 | return () => { 11 | clearTimeout(handler); 12 | }; 13 | }, [value, delay]); 14 | 15 | return debouncedValue; 16 | } 17 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/hooks/useEthBalance.ts: -------------------------------------------------------------------------------- 1 | import type { CoinQuantity } from 'fuels'; 2 | import { format } from 'fuels'; 3 | 4 | import { TOKENS, ETH } from '../utils'; 5 | 6 | import { useBalances } from './useBalances'; 7 | 8 | const ETH_ID = TOKENS.find((item) => item.symbol === ETH.symbol)?.assetId; 9 | 10 | export function useEthBalance() { 11 | const { data: balances } = useBalances(); 12 | const balance = balances?.find((item: CoinQuantity) => item.assetId === ETH_ID)?.amount; 13 | 14 | return { 15 | raw: balance, 16 | formatted: balance && format(balance), 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/hooks/useFuel.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export function useFuel() { 4 | const [error, setError] = useState(''); 5 | const [isLoading, setLoading] = useState(true); 6 | const [fuel, setFuel] = useState(); 7 | 8 | useEffect(() => { 9 | // Create a timeout to make sure it fails 10 | // in case fuel wallet is not install / detected 11 | const timeoutNotFound = setTimeout(() => { 12 | setLoading(false); 13 | clearTimeout(timeoutNotFound); 14 | setError('fuel not detected on the window!'); 15 | }, 2000); 16 | 17 | const onFuelLoaded = () => { 18 | setLoading(false); 19 | clearTimeout(timeoutNotFound); 20 | setError(''); 21 | setFuel(window.fuel); 22 | }; 23 | 24 | if (window.fuel) { 25 | onFuelLoaded(); 26 | } 27 | 28 | document.addEventListener('FuelLoaded', onFuelLoaded); 29 | 30 | // On unmount remove the event listener 31 | return () => { 32 | document.removeEventListener('FuelLoaded', onFuelLoaded); 33 | }; 34 | }, []); 35 | 36 | return { 37 | fuel, 38 | error, 39 | isLoading, 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/hooks/usePubSub.ts: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt'; 2 | import type { DependencyList } from 'react'; 3 | import { useEffect } from 'react'; 4 | 5 | const emitter = mitt(); 6 | 7 | type Listener = (data: T) => void; 8 | 9 | export function useSubscriber( 10 | event: string, 11 | listener: Listener, 12 | deps: DependencyList = [] 13 | ) { 14 | useEffect(() => { 15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 16 | emitter.on(event as any, listener as any); 17 | return () => emitter.off(event); 18 | }, deps); 19 | } 20 | 21 | export function usePublisher() { 22 | return emitter; 23 | } 24 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/hooks/useSlippage.ts: -------------------------------------------------------------------------------- 1 | import { SLIPPAGE_TOLERANCE } from '~/config'; 2 | 3 | export function useSlippage() { 4 | return { 5 | value: SLIPPAGE_TOLERANCE, 6 | formatted: `${SLIPPAGE_TOLERANCE * 100}%`, 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/hooks/useTokensMethods.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from 'react-query'; 2 | 3 | import { getOverrides } from '../utils/gas'; 4 | 5 | import { useWallet } from './useWallet'; 6 | 7 | import { TokenContractAbi__factory } from '~/types/contracts'; 8 | 9 | export function useTokenMethods(tokenId: string) { 10 | const { wallet, isLoading, isError } = useWallet(); 11 | 12 | const { data: methods, isLoading: isTokenMethodsLoading } = useQuery( 13 | ['TokenMethods', tokenId], 14 | async () => { 15 | const contract = TokenContractAbi__factory.connect(tokenId, wallet!); 16 | return { 17 | contract, 18 | getBalance() { 19 | // TODO fix 20 | return wallet?.getBalance(tokenId); 21 | }, 22 | queryNetworkFee() { 23 | return contract.functions.mint().txParams({ 24 | variableOutputs: 1, 25 | }); 26 | }, 27 | async getMintAmount() { 28 | const { value: mintAmount } = await contract.functions.get_mint_amount().get(); 29 | return mintAmount; 30 | }, 31 | async mint() { 32 | const { transactionResult } = await contract.functions 33 | .mint() 34 | .txParams( 35 | getOverrides({ 36 | variableOutputs: 1, 37 | }) 38 | ) 39 | .call(); 40 | return transactionResult; 41 | }, 42 | }; 43 | }, 44 | { 45 | enabled: Boolean(wallet && !isLoading && !isError), 46 | } 47 | ); 48 | 49 | return { methods, isLoading: isTokenMethodsLoading }; 50 | } 51 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/hooks/useTransactionCost.ts: -------------------------------------------------------------------------------- 1 | import type { FunctionInvocationScope, MultiCallInvocationScope } from 'fuels'; 2 | import type { UseQueryOptions } from 'react-query'; 3 | import { useQuery } from 'react-query'; 4 | 5 | import type { TransactionCost } from '../utils/gas'; 6 | import { emptyTransactionCost, getTransactionCost } from '../utils/gas'; 7 | 8 | import { useEthBalance } from './useEthBalance'; 9 | 10 | type ContractCallFuncPromise = () => Promise; 11 | type ContractCallFunc = () => FunctionInvocationScope | MultiCallInvocationScope; 12 | type UseTransactionCost = TransactionCost & { 13 | isLoading: boolean; 14 | }; 15 | 16 | export function useTransactionCost( 17 | queryKey: unknown[], 18 | request?: ContractCallFunc | ContractCallFuncPromise, 19 | options?: Omit, 'queryKey' | 'queryFn'> 20 | ): UseTransactionCost { 21 | const ethBalance = useEthBalance(); 22 | 23 | if (Array.isArray(queryKey)) { 24 | queryKey.push(ethBalance.formatted); 25 | } 26 | 27 | const { data, isLoading } = useQuery( 28 | queryKey, 29 | async () => { 30 | return getTransactionCost(await request!()); 31 | }, 32 | options 33 | ); 34 | 35 | return { 36 | ...(data || emptyTransactionCost()), 37 | isLoading, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/hooks/useWallet.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from 'react-query'; 2 | 3 | import { useFuel } from './useFuel'; 4 | 5 | export const useWallet = () => { 6 | const { fuel } = useFuel(); 7 | 8 | const { 9 | data: isConnected, 10 | isLoading: isConnectedLoading, 11 | isError: isConnectedError, 12 | } = useQuery( 13 | ['connected'], 14 | async () => { 15 | const isFuelConnected = await fuel!.isConnected(); 16 | return isFuelConnected; 17 | }, 18 | { 19 | enabled: !!fuel, 20 | initialData: false, 21 | } 22 | ); 23 | 24 | const { 25 | data: wallet, 26 | isLoading, 27 | isError, 28 | } = useQuery( 29 | ['wallet'], 30 | async () => { 31 | // The wallet should be connected as the user did it in the first step 32 | // We could add a check to see if the wallet is past the welcome steps 33 | // and still disconnected 34 | const currentAccount = await fuel!.currentAccount(); 35 | const currentWallet = (await fuel?.getWallet(currentAccount))!; 36 | return currentWallet; 37 | }, 38 | { 39 | enabled: !!fuel && !!isConnected && !isConnectedLoading && !isConnectedError, 40 | } 41 | ); 42 | 43 | return { wallet, isLoading, isError }; 44 | }; 45 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/hooks/useWalletConnection.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from 'react-query'; 2 | 3 | import { useFuel } from './useFuel'; 4 | 5 | export function useWalletConnection() { 6 | const { fuel } = useFuel(); 7 | const { 8 | data: isConnected, 9 | isLoading: isConnectedLoading, 10 | isError: isConnectedError, 11 | } = useQuery( 12 | ['connected'], 13 | async () => { 14 | const isFuelConnected = await fuel!.isConnected(); 15 | return isFuelConnected; 16 | }, 17 | { 18 | enabled: !!fuel, 19 | initialData: false, 20 | refetchInterval: 1000, 21 | } 22 | ); 23 | return { 24 | isConnected, 25 | isConnectedLoading, 26 | isConnectedError, 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./components"; 2 | export * from "./hooks"; 3 | export * from "./utils"; 4 | export * from "./context"; 5 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/utils/chain.ts: -------------------------------------------------------------------------------- 1 | import type { BigNumberish, Contract } from 'fuels'; 2 | 3 | import { DEADLINE } from '~/config'; 4 | 5 | export async function getDeadline(contract: Contract, deadline?: BigNumberish) { 6 | const blockHeight = await contract.provider?.getBlockNumber(); 7 | const nexDeadline = blockHeight?.add(deadline || DEADLINE); 8 | return nexDeadline!; 9 | } 10 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/utils/coins.ts: -------------------------------------------------------------------------------- 1 | import type { CoinQuantity } from 'fuels'; 2 | import { NativeAssetId } from 'fuels'; 3 | 4 | export const getCoin = (coinsQuantity: Array, assetId?: string) => { 5 | return coinsQuantity.find((cq) => cq.assetId === assetId); 6 | }; 7 | 8 | export const getCoinETH = (coinsQuantity: Array) => { 9 | return coinsQuantity.find((cq) => cq.assetId === NativeAssetId); 10 | }; 11 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/utils/constants.ts: -------------------------------------------------------------------------------- 1 | import { TOKENS } from './tokenList'; 2 | 3 | export const contractABI = {}; 4 | export const contractAddress = '0xF93c18172eAba6a9F145B3FB16d2bBeA2e096477'; 5 | export const LocalStorageKey = 'swayswap-v4'; 6 | export const COIN_ETH = TOKENS[0].assetId; 7 | export const MIN_GAS_AMOUNT_ADD_LIQUIDITY = 5; 8 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/utils/feedback.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import type { TransactionResult } from "fuels"; 3 | import toast from "react-hot-toast"; 4 | 5 | import { BLOCK_EXPLORER_URL } from "~/config"; 6 | import { Link } from "~/systems/UI"; 7 | import type { Maybe } from "~/types"; 8 | 9 | export function getBlockExplorerLink(path: string) { 10 | return `${BLOCK_EXPLORER_URL}${path}?providerUrl=${encodeURIComponent( 11 | process.env.VITE_FUEL_PROVIDER_URL as string 12 | )}`; 13 | } 14 | 15 | type TxLinkProps = { 16 | id?: string; 17 | }; 18 | 19 | export function TxLink({ id }: TxLinkProps) { 20 | return ( 21 |

22 | 23 | View it on Fuel Explorer 24 | 25 |

26 | ); 27 | } 28 | 29 | export function txFeedback( 30 | txMsg: string, 31 | onSuccess?: (data: TransactionResult) => void | Promise 32 | ) { 33 | return async (data: Maybe>) => { 34 | const txLink = ; 35 | 36 | /** 37 | * Show a toast success message if status.type === 'success' 38 | */ 39 | if (data?.status.type === "success") { 40 | await onSuccess?.(data); 41 | toast.success( 42 | <> 43 | {" "} 44 | {txMsg} {txLink}{" "} 45 | , 46 | { duration: 8000 } 47 | ); 48 | return; 49 | } 50 | 51 | /** 52 | * Show a toast error if status.type !== 'success'' 53 | */ 54 | toast.error(<>Transaction reverted! {txLink}); 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/utils/helpers.ts: -------------------------------------------------------------------------------- 1 | import { TOKENS } from './tokenList'; 2 | 3 | import type { Coin, Maybe } from '~/types'; 4 | 5 | export const objectId = (value: string) => ({ value }); 6 | 7 | export function sleep(ms: number) { 8 | return new Promise((resolve) => { 9 | setTimeout(resolve, ms); 10 | }); 11 | } 12 | 13 | // eslint-disable-next-line @typescript-eslint/ban-types 14 | export function omit(list: string[], props: T) { 15 | return Object.entries(props).reduce((obj, [key, value]) => { 16 | if (list.some((k) => k === key)) return obj; 17 | return { ...obj, [key]: value }; 18 | }, {} as T) as T; 19 | } 20 | 21 | export function isCoinEth(coin: Maybe) { 22 | return coin?.assetId === TOKENS[0].assetId; 23 | } 24 | 25 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 26 | export function compareStates(a: any, b: any) { 27 | return JSON.stringify(a) === JSON.stringify(b); 28 | } 29 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | export * from './helpers'; 3 | export * from './math'; 4 | export * from './queryClient'; 5 | export * from './relativeUrl'; 6 | export * from './tokenList'; 7 | export * from './chain'; 8 | export * from './coins'; 9 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/utils/math.test.ts: -------------------------------------------------------------------------------- 1 | import { bn } from 'fuels'; 2 | 3 | import * as math from './math'; 4 | 5 | describe('Math utilities', () => { 6 | it('Math.multiply', () => { 7 | expect(math.multiply(bn(100), 1.2).toHex()).toEqual(bn(120).toHex()); 8 | expect(math.multiply(bn(2), 0.5).toHex()).toEqual(bn(1).toHex()); 9 | expect(math.multiply(bn('100000020000'), 0.5).toHex()).toEqual(bn('50000010000').toHex()); 10 | }); 11 | 12 | it('Math.divide', () => { 13 | expect(math.divide(bn(4), bn(2)).toHex()).toEqual(bn(2).toHex()); 14 | expect(math.divide(bn(1), 0.5).toHex()).toEqual(bn(2).toHex()); 15 | expect(math.divide(bn('100000020000'), 0.5).toHex()).toEqual(bn('200000040000').toHex()); 16 | }); 17 | 18 | it('Math.minimumZero', () => { 19 | expect(math.minimumZero('-2').toHex()).toEqual(math.ZERO.toHex()); 20 | }); 21 | 22 | it('Math.maxAmount', () => { 23 | expect(math.maxAmount(bn(1), bn(2)).toHex()).toEqual(bn(2).toHex()); 24 | }); 25 | 26 | it('Math.isValidNumber', () => { 27 | expect(math.isValidNumber(bn('0xFFFFFFFFFFFFFFFF'))).toBeTruthy(); 28 | expect(math.isValidNumber(bn('0x1FFFFFFFFFFFFFFFF'))).toBeFalsy(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/utils/math.ts: -------------------------------------------------------------------------------- 1 | import { Decimal } from 'decimal.js'; 2 | import { bn } from 'fuels'; 3 | import type { BigNumberish, BN } from 'fuels'; 4 | 5 | import { DECIMAL_UNITS } from '~/config'; 6 | import type { Maybe } from '~/types'; 7 | 8 | /** Zero BN function */ 9 | export const ZERO = bn(0); 10 | /** One Asset Amount 1000000000 */ 11 | export const ONE_ASSET = bn.parseUnits('1', DECIMAL_UNITS); 12 | /** Max value from Sway Contract */ 13 | export const MAX_U64_STRING = '0xFFFFFFFFFFFFFFFF'; 14 | 15 | export function getNumberOrHex(value: Maybe): number | string { 16 | if (typeof value === 'number') { 17 | return value; 18 | } 19 | return bn(value || 0).toHex(); 20 | } 21 | 22 | export function multiply(value?: Maybe, by?: Maybe): BN { 23 | return bn(new Decimal(getNumberOrHex(value)).mul(getNumberOrHex(by)).round().toHex()); 24 | } 25 | 26 | export function divide(value?: Maybe, by?: Maybe): BN { 27 | return bn(new Decimal(getNumberOrHex(value)).div(getNumberOrHex(by)).round().toHex()); 28 | } 29 | 30 | export function minimumZero(value: BigNumberish): BN { 31 | return bn(value).lte('0') ? bn('0') : bn(value); 32 | } 33 | 34 | export function maxAmount(value: BigNumberish, max: BigNumberish): BN { 35 | return bn(max).lt(value) ? bn(value) : bn(max); 36 | } 37 | 38 | export function isValidNumber(value: BigNumberish) { 39 | try { 40 | if (typeof value === 'string') { 41 | return bn.parseUnits(value).lte(MAX_U64_STRING); 42 | } 43 | return bn(value).lte(MAX_U64_STRING); 44 | } catch (e) { 45 | return false; 46 | } 47 | } 48 | 49 | export function calculatePercentage(amount: BN, by: BN) { 50 | return new Decimal(amount.toHex()).div(by.toHex()).mul(100); 51 | } 52 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/utils/queryClient.tsx: -------------------------------------------------------------------------------- 1 | import { copy } from "clipboard"; 2 | import toast from "react-hot-toast"; 3 | import { QueryClient } from "react-query"; 4 | 5 | const panicError = (msg: string) => ( 6 |
7 | Unexpected block execution error 8 |
9 | 10 | copy(msg)}> 11 | Click here 12 | {" "} 13 | to copy block response 14 | 15 |
16 | ); 17 | 18 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 19 | export function handleError(error: any) { 20 | const msg = error?.message; 21 | toast.error(msg?.includes("Panic") ? panicError(msg) : msg, { 22 | duration: 100000000, 23 | id: msg, 24 | }); 25 | } 26 | 27 | export const queryClient = new QueryClient({ 28 | defaultOptions: { 29 | queries: { 30 | onError: handleError, 31 | // These two are annoying during development 32 | retry: false, 33 | refetchOnWindowFocus: false, 34 | // This is disabled because it causes a bug with arrays with named keys 35 | // For example, if a query returns: [BN, BN, a: BN, b: BN] 36 | // with this option on it will be cached as: [BN, BN] 37 | // and break our code 38 | structuralSharing: false, 39 | }, 40 | mutations: { 41 | onError: handleError, 42 | }, 43 | }, 44 | }); 45 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/utils/relativeUrl.ts: -------------------------------------------------------------------------------- 1 | import { urlJoin } from 'url-join-ts'; 2 | 3 | export function relativeUrl(path: string) { 4 | return urlJoin(window.location.origin, process.env.PUBLIC_URL, path); 5 | } 6 | -------------------------------------------------------------------------------- /packages/app/src/systems/Core/utils/tokenList.ts: -------------------------------------------------------------------------------- 1 | import { relativeUrl } from './relativeUrl'; 2 | 3 | import { CONTRACT_ID, TOKEN_ID1, TOKEN_ID2 } from '~/config'; 4 | import type { Coin } from '~/types'; 5 | 6 | export const ASSET_404 = { 7 | name: '404', 8 | img: relativeUrl('/icons/other.svg'), 9 | }; 10 | 11 | export const ETH = { 12 | name: 'sEther', 13 | symbol: 'sETH', 14 | assetId: TOKEN_ID1, 15 | img: relativeUrl('/icons/eth.svg'), 16 | }; 17 | 18 | export const DAI = { 19 | name: 'DAI', 20 | symbol: 'DAI', 21 | // TODO: Remove this when adding dynamic token insertion 22 | // Make temporarily easy to change token contract id 23 | // https://github.com/FuelLabs/swayswap-demo/issues/33 24 | assetId: TOKEN_ID2, 25 | img: relativeUrl('/icons/dai.svg'), 26 | }; 27 | 28 | export const ETH_DAI = { 29 | name: 'sETH/DAI', 30 | symbol: 'sETH/DAI', 31 | // TODO: Remove this when adding dynamic token insertion 32 | // Make temporarily easy to change token contract id 33 | // https://github.com/FuelLabs/swayswap-demo/issues/33 34 | assetId: CONTRACT_ID, 35 | img: relativeUrl('/icons/eth_dai.svg'), 36 | pairOf: [ETH, DAI], 37 | }; 38 | 39 | export const TOKENS: Array = [ETH, DAI, ETH_DAI]; 40 | -------------------------------------------------------------------------------- /packages/app/src/systems/Faucet/components/FaucetDialog.tsx: -------------------------------------------------------------------------------- 1 | import toast from "react-hot-toast"; 2 | import { FaFaucet } from "react-icons/fa"; 3 | 4 | import { useFaucetDialog } from "../hooks"; 5 | import { useFaucet } from "../hooks/useFaucet"; 6 | 7 | import { FaucetApp } from "./FaucetApp"; 8 | 9 | import { Card, Dialog } from "~/systems/UI"; 10 | 11 | export function FaucetDialog() { 12 | const faucet = useFaucet(); 13 | const dialog = useFaucetDialog(); 14 | return ( 15 | 16 | 17 | 18 | 19 | Faucet 20 | 21 |
22 | Click the button below to receive {faucet.faucetAmount} test ETH to 23 | your wallet. 24 |
25 | { 28 | toast.success("Test ETH successfully fauceted!"); 29 | dialog.close(); 30 | }} 31 | /> 32 |
33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /packages/app/src/systems/Faucet/components/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./FaucetApp"; 2 | export * from "./FaucetDialog"; 3 | -------------------------------------------------------------------------------- /packages/app/src/systems/Faucet/hooks/__mocks__/useFaucet.ts: -------------------------------------------------------------------------------- 1 | import type { FuelWalletLocked } from '@fuel-wallet/sdk'; 2 | 3 | import { fetchFaucet } from '../useFaucet'; 4 | 5 | export async function faucet(wallet: FuelWalletLocked, times = 2) { 6 | const range = Array.from({ length: times }); 7 | await range.reduce(async (promise) => { 8 | await promise; 9 | return fetchFaucet({ 10 | method: 'POST', 11 | body: JSON.stringify({ 12 | address: wallet?.address.toAddress(), 13 | captcha: '', 14 | }), 15 | }); 16 | }, Promise.resolve()); 17 | } 18 | -------------------------------------------------------------------------------- /packages/app/src/systems/Faucet/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useCaptcha'; 2 | export * from './useFaucet'; 3 | export * from './useFaucetDialog'; 4 | -------------------------------------------------------------------------------- /packages/app/src/systems/Faucet/hooks/useFaucetDialog.tsx: -------------------------------------------------------------------------------- 1 | import { atom, useAtom } from "jotai"; 2 | 3 | import { useDialog } from "~/systems/UI"; 4 | 5 | const dialogAtom = atom(false); 6 | 7 | export function useFaucetDialog() { 8 | const [opened, setOpened] = useAtom(dialogAtom); 9 | return useDialog({ 10 | isOpen: opened, 11 | onOpenChange: setOpened, 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /packages/app/src/systems/Faucet/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./components"; 2 | export * from "./hooks"; 3 | -------------------------------------------------------------------------------- /packages/app/src/systems/Home/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { FuelLogo } from "@fuel-ui/react"; 2 | import { useNavigate } from "react-router-dom"; 3 | 4 | import { useBreakpoint } from "~/systems/Core"; 5 | import { Button, Link } from "~/systems/UI"; 6 | import { Pages } from "~/types"; 7 | 8 | export function Header() { 9 | const breakpoint = useBreakpoint(); 10 | const navigate = useNavigate(); 11 | return ( 12 |
13 | 14 | 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /packages/app/src/systems/Home/components/HomeHero.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from "react-router-dom"; 2 | 3 | import { Button, Link } from "~/systems/UI"; 4 | import { Pages } from "~/types"; 5 | 6 | export function HomeHero() { 7 | const navigate = useNavigate(); 8 | return ( 9 |
10 |

11 | SwaySwap is a blazingly fast DEX built on the fastest modular execution 12 | layer: Fuel. 13 |

14 |

15 | Built with an entirely new language{" "} 16 | 17 | [Sway] 18 | 19 | , virtual machine{" "} 20 | 21 | [FuelVM] 22 | 23 | , and UTXO-based smart contract blockchain{" "} 24 | 25 | [Fuel] 26 | 27 | , you can now experience a demonstration of the next generation of 28 | scaling beyond layer-2s and monolithic blockchain design. 29 | #BeyondMonolithic 30 |

31 |

32 | This is running on the Fuel test network. No real funds are used. 33 | Demonstration purposes only. 34 |

35 | 38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /packages/app/src/systems/Home/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./routes"; 2 | -------------------------------------------------------------------------------- /packages/app/src/systems/Home/pages/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import { Header } from "../components/Header"; 2 | import { HomeHero } from "../components/HomeHero"; 3 | 4 | export function HomePage() { 5 | return ( 6 |
7 |
8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /packages/app/src/systems/Home/pages/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./HomePage"; 2 | -------------------------------------------------------------------------------- /packages/app/src/systems/Home/routes.tsx: -------------------------------------------------------------------------------- 1 | import { Route } from "react-router-dom"; 2 | 3 | import { HomePage } from "./pages"; 4 | 5 | import { Pages } from "~/types"; 6 | 7 | export const homeRoutes = } />; 8 | -------------------------------------------------------------------------------- /packages/app/src/systems/Mint/hooks/__mocks__/useMint.ts: -------------------------------------------------------------------------------- 1 | import type { FuelWalletLocked } from '@fuel-wallet/sdk'; 2 | 3 | import { TOKEN_ID1, TOKEN_ID2 } from '~/config'; 4 | import { getOverrides } from '~/systems/Core/utils/gas'; 5 | import { TokenContractAbi__factory } from '~/types/contracts'; 6 | 7 | export async function mint(wallet: FuelWalletLocked) { 8 | const tokenContract1 = TokenContractAbi__factory.connect(TOKEN_ID1, wallet); 9 | const tokenContract2 = TokenContractAbi__factory.connect(TOKEN_ID2, wallet); 10 | 11 | return tokenContract1 12 | .multiCall([tokenContract1.functions.mint(), tokenContract2.functions.mint()]) 13 | .txParams(getOverrides({ variableOutputs: 1 })) 14 | .call(); 15 | } 16 | -------------------------------------------------------------------------------- /packages/app/src/systems/Mint/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useMint'; 2 | -------------------------------------------------------------------------------- /packages/app/src/systems/Mint/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./routes"; 2 | export * from "./hooks"; 3 | -------------------------------------------------------------------------------- /packages/app/src/systems/Mint/pages/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./MintPage"; 2 | -------------------------------------------------------------------------------- /packages/app/src/systems/Mint/routes.tsx: -------------------------------------------------------------------------------- 1 | import { Route } from "react-router-dom"; 2 | 3 | import { PrivateRoute } from "../Core"; 4 | 5 | import { MintPage } from "./pages"; 6 | 7 | import { Pages } from "~/types"; 8 | 9 | export const mintRoutes = ( 10 | 14 | 15 | 16 | } 17 | /> 18 | ); 19 | -------------------------------------------------------------------------------- /packages/app/src/systems/Pool/components/AddLiquidityPoolPrice.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "@xstate/react"; 2 | import Decimal from "decimal.js"; 3 | import { bn, format } from "fuels"; 4 | import { useMemo } from "react"; 5 | 6 | import { useAddLiquidityContext } from "../hooks"; 7 | import { selectors } from "../selectors"; 8 | 9 | import { ONE_ASSET } from "~/systems/Core"; 10 | 11 | export const AddLiquidityPoolPrice = () => { 12 | const { service } = useAddLiquidityContext(); 13 | const coinFrom = useSelector(service, selectors.coinFrom); 14 | const coinTo = useSelector(service, selectors.coinTo); 15 | const poolRatio = useSelector(service, selectors.poolRatio) || 1; 16 | 17 | const [daiPrice, ethPrice] = useMemo(() => { 18 | const daiPriceFormatted = format( 19 | bn(new Decimal(ONE_ASSET.toHex()).div(poolRatio).round().toHex()) 20 | ); 21 | const ethPriceFormatted = format( 22 | bn(new Decimal(ONE_ASSET.toHex()).mul(poolRatio).round().toHex()) 23 | ); 24 | return [daiPriceFormatted, ethPriceFormatted]; 25 | }, [poolRatio]); 26 | 27 | return ( 28 |
29 |

Pool price

30 |
31 |
32 | 33 | 1 {coinFrom.name} = {daiPrice} {coinTo.name} 34 | 35 | 36 | 1 {coinTo.name} = {ethPrice} {coinFrom.name} 37 | 38 |
39 |
40 |
41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /packages/app/src/systems/Pool/components/NewPoolWarning.tsx: -------------------------------------------------------------------------------- 1 | const style = { 2 | createPoolInfo: `font-mono mt-4 px-4 py-3 text-sm text-slate-400 decoration-1 border border-dashed 3 | border-white/10 rounded-xl w-full`, 4 | }; 5 | 6 | export const NewPoolWarning = () => ( 7 |
8 |

9 | You are creating a new pool 10 |

11 |
12 | You are the first to provide liquidity to this pool. The ratio between 13 | these tokens will set the price of this pool. 14 |
15 |
16 | ); 17 | -------------------------------------------------------------------------------- /packages/app/src/systems/Pool/components/PoolCurrentPosition.tsx: -------------------------------------------------------------------------------- 1 | import { useUserPositions } from "../hooks"; 2 | 3 | import { 4 | PreviewTable, 5 | PreviewItem, 6 | TokenIcon, 7 | useCoinMetadata, 8 | ETH_DAI, 9 | } from "~/systems/Core"; 10 | 11 | export const PoolCurrentPosition = () => { 12 | const info = useUserPositions(); 13 | const { coinMetaData } = useCoinMetadata({ symbol: ETH_DAI.name }); 14 | const coinFrom = coinMetaData?.pairOf?.[0]; 15 | const coinTo = coinMetaData?.pairOf?.[1]; 16 | 17 | return ( 18 | 19 | 23 | {info.formattedPooledDAI} 24 | 25 | } 26 | /> 27 | 31 | {info.formattedPooledETH}{" "} 32 | 33 | 34 | } 35 | /> 36 | 40 | {info.formattedPoolTokens}{" "} 41 | 42 | 43 | } 44 | /> 45 | 49 | 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /packages/app/src/systems/Pool/components/PoolCurrentReserves.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "@xstate/react"; 2 | import { bn, format } from "fuels"; 3 | 4 | import { useAddLiquidityContext } from "../hooks"; 5 | import { selectors } from "../selectors"; 6 | 7 | import { NewPoolWarning } from "./NewPoolWarning"; 8 | 9 | import { 10 | PreviewTable, 11 | PreviewItem, 12 | TokenIcon, 13 | compareStates, 14 | } from "~/systems/Core"; 15 | 16 | export const PoolCurrentReserves = () => { 17 | const { service } = useAddLiquidityContext(); 18 | const poolInfo = useSelector(service, selectors.poolInfo, compareStates); 19 | const coinFrom = useSelector(service, selectors.coinFrom); 20 | const createPool = useSelector(service, selectors.createPool); 21 | const coinTo = useSelector(service, selectors.coinTo); 22 | const isLoading = useSelector(service, selectors.isLoading); 23 | 24 | if (!poolInfo) return null; 25 | if (createPool) return ; 26 | 27 | return ( 28 | 33 | 37 | 38 | {coinFrom?.name} 39 | 40 | } 41 | value={format(bn(poolInfo?.token_reserve1))} 42 | /> 43 | 47 | 48 | {coinTo?.name} 49 | 50 | } 51 | value={format(bn(poolInfo?.token_reserve2))} 52 | /> 53 | 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /packages/app/src/systems/Pool/components/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./AddLiquidityPoolPrice"; 2 | export * from "./AddLiquidityPreview"; 3 | export * from "./PoolCurrentPosition"; 4 | export * from "./PoolCurrentReserves"; 5 | export * from "./RemoveLiquidityPreview"; 6 | export * from "./NewPoolWarning"; 7 | export * from "./AddLiquidityButton"; 8 | -------------------------------------------------------------------------------- /packages/app/src/systems/Pool/hooks/__mocks__/addLiquidity.ts: -------------------------------------------------------------------------------- 1 | import type { FuelWalletLocked } from '@fuel-wallet/sdk'; 2 | import { bn } from 'fuels'; 3 | 4 | import { CONTRACT_ID } from '~/config'; 5 | import { getDeadline } from '~/systems/Core'; 6 | import { getOverrides } from '~/systems/Core/utils/gas'; 7 | import { ExchangeContractAbi__factory } from '~/types/contracts'; 8 | 9 | export async function addLiquidity( 10 | wallet: FuelWalletLocked, 11 | fromAmount: string, 12 | toAmount: string, 13 | fromAsset: string, 14 | toAsset: string 15 | ) { 16 | const contract = ExchangeContractAbi__factory.connect(CONTRACT_ID, wallet); 17 | const deadline = await getDeadline(contract); 18 | const { transactionResult } = await contract 19 | .multiCall([ 20 | contract.functions.deposit().callParams({ 21 | forward: [bn.parseUnits(fromAmount), fromAsset], 22 | }), 23 | contract.functions.deposit().callParams({ 24 | forward: [bn.parseUnits(toAmount), toAsset], 25 | }), 26 | contract.functions.add_liquidity(1, deadline).callParams({ 27 | forward: [bn(0), toAsset], 28 | }), 29 | ]) 30 | .txParams( 31 | getOverrides({ 32 | variableOutputs: 2, 33 | gasLimit: 20000000, 34 | }) 35 | ) 36 | .call(); 37 | 38 | return transactionResult; 39 | } 40 | -------------------------------------------------------------------------------- /packages/app/src/systems/Pool/hooks/__mocks__/useUserPosition.ts: -------------------------------------------------------------------------------- 1 | import Decimal from 'decimal.js'; 2 | 3 | import type { PoolInfoPreview } from '../../utils/helpers'; 4 | import * as useUserPositions from '../useUserPositions'; 5 | 6 | import { ZERO } from '~/systems/Core'; 7 | 8 | const NO_POSITIONS: PoolInfoPreview = { 9 | ethReserve: ZERO, 10 | formattedEthReserve: '0.0', 11 | formattedPoolShare: '0.0', 12 | formattedPoolTokens: '0', 13 | formattedPooledDAI: '0.0', 14 | formattedPooledETH: '0.0', 15 | formattedTokenReserve: '0.0', 16 | hasPositions: false, 17 | poolRatio: new Decimal(0), 18 | poolShare: new Decimal(0), 19 | poolTokens: ZERO, 20 | pooledDAI: ZERO, 21 | pooledETH: ZERO, 22 | tokenReserve: ZERO, 23 | totalLiquidity: ZERO, 24 | }; 25 | 26 | export function mockUseUserPosition(opts?: Partial) { 27 | return jest.spyOn(useUserPositions, 'useUserPositions').mockImplementation(() => ({ 28 | ...NO_POSITIONS, 29 | ...opts, 30 | })); 31 | } 32 | -------------------------------------------------------------------------------- /packages/app/src/systems/Pool/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useAddLiquidity'; 2 | export * from './usePoolInfo'; 3 | export * from './usePreviewRemoveLiquidity'; 4 | export * from './useUserPositions'; 5 | -------------------------------------------------------------------------------- /packages/app/src/systems/Pool/hooks/usePoolInfo.ts: -------------------------------------------------------------------------------- 1 | import type { BN } from 'fuels'; 2 | import { bn } from 'fuels'; 3 | import { useQuery } from 'react-query'; 4 | 5 | import { useContract } from '~/systems/Core'; 6 | 7 | export function usePoolInfo() { 8 | const contract = useContract(); 9 | return useQuery('PoolPage-poolInfo', async () => { 10 | if (!contract) return; 11 | const { value: poolInfo } = await contract.functions.get_pool_info().get({ 12 | fundTransaction: false, 13 | }); 14 | return poolInfo; 15 | }); 16 | } 17 | 18 | export function usePositionInfo(amount: BN) { 19 | const contract = useContract(); 20 | return useQuery(['PoolPage-positionInfo', amount], async () => { 21 | if (!contract || bn(amount).isZero()) return; 22 | const { value: position } = await contract.functions.get_position(amount).get(); 23 | return position; 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /packages/app/src/systems/Pool/hooks/usePreviewRemoveLiquidity.ts: -------------------------------------------------------------------------------- 1 | import Decimal from 'decimal.js'; 2 | import type { BN } from 'fuels'; 3 | import { bn, format, toFixed } from 'fuels'; 4 | import { useMemo } from 'react'; 5 | 6 | import type { PoolInfoPreview } from '../utils/helpers'; 7 | import { getPoolInfoPreview } from '../utils/helpers'; 8 | 9 | import { usePositionInfo } from './usePoolInfo'; 10 | 11 | import { CONTRACT_ID } from '~/config'; 12 | import { calculatePercentage, getCoin, useBalances, ZERO } from '~/systems/Core'; 13 | import type { Maybe } from '~/types'; 14 | 15 | export type PreviewRemoveLiquidity = PoolInfoPreview & { 16 | formattedNextPoolTokens: string; 17 | formattedNextPoolShare: string; 18 | }; 19 | 20 | export function usePreviewRemoveLiquidity(amount: Maybe): PreviewRemoveLiquidity { 21 | const { data: balances } = useBalances(); 22 | const poolTokens = useMemo(() => { 23 | const lpTokenAmount = getCoin(balances || [], CONTRACT_ID)?.amount; 24 | const poolTokensNum = bn(lpTokenAmount); 25 | return poolTokensNum; 26 | }, [balances]); 27 | const { data: info } = usePositionInfo(amount || ZERO); 28 | const poolPreview = useMemo(() => getPoolInfoPreview(info, amount || ZERO), [info, amount]); 29 | let nextPoolTokens = ZERO; 30 | let nextPoolShare = new Decimal(0); 31 | 32 | if (!bn(poolTokens).isZero()) { 33 | nextPoolTokens = poolTokens.sub(poolPreview.poolTokens); 34 | if (nextPoolTokens.lte(poolPreview.totalLiquidity)) { 35 | nextPoolShare = calculatePercentage(nextPoolTokens, poolPreview.totalLiquidity); 36 | } 37 | } 38 | 39 | const formattedNextPoolTokens = format(nextPoolTokens); 40 | const formattedNextPoolShare = toFixed(nextPoolShare.toString()); 41 | 42 | return { 43 | ...poolPreview, 44 | formattedNextPoolTokens, 45 | formattedNextPoolShare, 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /packages/app/src/systems/Pool/hooks/useUserPositions.ts: -------------------------------------------------------------------------------- 1 | import { bn } from 'fuels'; 2 | import { useMemo } from 'react'; 3 | 4 | import { getPoolInfoPreview } from '../utils/helpers'; 5 | 6 | import { usePositionInfo } from './usePoolInfo'; 7 | 8 | import { CONTRACT_ID } from '~/config'; 9 | import { useBalances, getCoin } from '~/systems/Core'; 10 | 11 | export function useUserPositions() { 12 | const { data: balances } = useBalances(); 13 | const poolTokens = useMemo(() => { 14 | const lpTokenAmount = getCoin(balances || [], CONTRACT_ID)?.amount; 15 | const poolTokensNum = bn(lpTokenAmount); 16 | return poolTokensNum; 17 | }, [balances]); 18 | const { data: info } = usePositionInfo(poolTokens); 19 | const poolPreview = useMemo(() => getPoolInfoPreview(info, poolTokens), [info, poolTokens]); 20 | 21 | return poolPreview; 22 | } 23 | -------------------------------------------------------------------------------- /packages/app/src/systems/Pool/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./hooks"; 2 | export * from "./pages"; 3 | export * from "./utils"; 4 | export * from "./routes"; 5 | -------------------------------------------------------------------------------- /packages/app/src/systems/Pool/pages/PoolPage.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from "react-router-dom"; 2 | 3 | import { MainLayout, PrivateRoute } from "~/systems/Core"; 4 | 5 | export function PoolPage() { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /packages/app/src/systems/Pool/pages/Pools.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, renderWithRouter } from "@swayswap/test-utils"; 2 | 3 | import { App } from "~/App"; 4 | import { mockUserData } from "~/systems/Core/hooks/__mocks__/useWallet"; 5 | 6 | describe("Pool List", () => { 7 | beforeEach(() => { 8 | mockUserData(); 9 | }); 10 | 11 | afterEach(() => { 12 | jest.clearAllMocks(); 13 | }); 14 | 15 | it("should render with no position first", async () => { 16 | renderWithRouter(, { route: "/pool/list" }); 17 | 18 | const noPositions = await screen.findByText( 19 | /you do not have any open positions/i 20 | ); 21 | expect(noPositions).toBeInTheDocument(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/app/src/systems/Pool/pages/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./AddLiquidity"; 2 | export * from "./PoolPage"; 3 | export * from "./Pools"; 4 | export * from "./RemoveLiquidity"; 5 | -------------------------------------------------------------------------------- /packages/app/src/systems/Pool/portals/AddLiquidityPortal.tsx: -------------------------------------------------------------------------------- 1 | import { AddLiquidityProvider } from "../hooks/useAddLiquidity"; 2 | import { AddLiquidity } from "../pages"; 3 | 4 | export const AddLiquidityPortal = () => ( 5 | 6 | 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /packages/app/src/systems/Pool/routes.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate, Route } from "react-router-dom"; 2 | 3 | import { PoolPage, RemoveLiquidityPage, Pools } from "./pages"; 4 | import { AddLiquidityPortal } from "./portals/AddLiquidityPortal"; 5 | 6 | import { Pages } from "~/types"; 7 | 8 | export const poolRoutes = ( 9 | }> 10 | } /> 11 | } /> 12 | } /> 13 | } 16 | /> 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /packages/app/src/systems/Pool/selectors.ts: -------------------------------------------------------------------------------- 1 | import { bn } from 'fuels'; 2 | 3 | import type { AddLiquidityMachineState } from './machines/addLiquidityMachine'; 4 | import type { AddLiquidityActive } from './types'; 5 | 6 | export const selectors = { 7 | createPool: ({ context: ctx }: AddLiquidityMachineState) => { 8 | return bn(ctx.poolInfo?.token_reserve1).isZero(); 9 | }, 10 | addLiquidity: ({ context: ctx }: AddLiquidityMachineState) => { 11 | return ctx.poolInfo?.token_reserve1.gt(0); 12 | }, 13 | poolShare: ({ context: ctx }: AddLiquidityMachineState) => { 14 | return ctx.poolShare; 15 | }, 16 | fromAmount: ({ context: ctx }: AddLiquidityMachineState) => { 17 | return ctx.fromAmount; 18 | }, 19 | toAmount: ({ context: ctx }: AddLiquidityMachineState) => { 20 | return ctx.toAmount; 21 | }, 22 | coinFrom: ({ context: ctx }: AddLiquidityMachineState) => { 23 | return ctx.coinFrom; 24 | }, 25 | coinTo: ({ context: ctx }: AddLiquidityMachineState) => { 26 | return ctx.coinTo; 27 | }, 28 | isActiveLoading: (active: AddLiquidityActive) => (state: AddLiquidityMachineState) => { 29 | return ( 30 | active !== state.context.active && 31 | state.hasTag('loading') && 32 | !bn(state.context.poolInfo?.token_reserve1).isZero() 33 | ); 34 | }, 35 | previewAmount: ({ context: ctx }: AddLiquidityMachineState) => { 36 | return bn(ctx.liquidityPreview?.liquidityTokens); 37 | }, 38 | poolInfo: ({ context: ctx }: AddLiquidityMachineState) => { 39 | return ctx.poolInfo; 40 | }, 41 | isLoading: (state: AddLiquidityMachineState) => { 42 | return state.hasTag('loading'); 43 | }, 44 | transactionCost: ({ context: ctx }: AddLiquidityMachineState) => { 45 | return ctx.transactionCost; 46 | }, 47 | poolRatio: ({ context: ctx }: AddLiquidityMachineState) => { 48 | return ctx.poolRatio; 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /packages/app/src/systems/Pool/state.ts: -------------------------------------------------------------------------------- 1 | import type { BN } from 'fuels'; 2 | import { atom } from 'jotai'; 3 | import { atomWithReset, useResetAtom } from 'jotai/utils'; 4 | 5 | import type { Maybe } from '~/types'; 6 | 7 | export const poolFromAmountAtom = atomWithReset>(null); 8 | export const poolToAmountAtom = atomWithReset>(null); 9 | 10 | export const poolStageDoneAtom = atom(0); 11 | 12 | export const useResetPoolAmounts = () => { 13 | const resetFromAmount = useResetAtom(poolFromAmountAtom); 14 | const resetToAmount = useResetAtom(poolToAmountAtom); 15 | return () => { 16 | resetFromAmount(); 17 | resetToAmount(); 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/app/src/systems/Pool/types.ts: -------------------------------------------------------------------------------- 1 | import type Decimal from 'decimal.js'; 2 | import type { BN, CoinQuantity } from 'fuels'; 3 | import type { QueryClient } from 'react-query'; 4 | import type { NavigateFunction } from 'react-router-dom'; 5 | 6 | import { ZERO } from '../Core'; 7 | import type { TransactionCost } from '../Core/utils/gas'; 8 | 9 | import type { Coin, Maybe } from '~/types'; 10 | import type { ExchangeContractAbi } from '~/types/contracts'; 11 | import type { PoolInfoOutput } from '~/types/contracts/ExchangeContractAbi'; 12 | 13 | export enum AddLiquidityActive { 14 | 'from' = 'from', 15 | 'to' = 'to', 16 | } 17 | 18 | export type LiquidityPreview = { 19 | liquidityTokens: BN; 20 | requiredAmount: BN; 21 | }; 22 | 23 | export type AddLiquidityMachineContext = { 24 | navigate: NavigateFunction; 25 | client: QueryClient; 26 | coinFrom: Coin; 27 | coinTo: Coin; 28 | active: Maybe; 29 | fromAmount: Maybe; 30 | toAmount: Maybe; 31 | contract: Maybe; 32 | liquidityPreview: LiquidityPreview; 33 | poolShare: Decimal; 34 | poolInfo: Maybe; 35 | poolRatio: Maybe; 36 | poolPosition: Maybe; 37 | balances: Array; 38 | transactionCost: Maybe; 39 | }; 40 | 41 | export const liquidityPreviewEmpty: LiquidityPreview = { 42 | liquidityTokens: ZERO, 43 | requiredAmount: ZERO, 44 | }; 45 | -------------------------------------------------------------------------------- /packages/app/src/systems/Pool/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './helpers'; 2 | export * from './queries'; 3 | -------------------------------------------------------------------------------- /packages/app/src/systems/Swap/components/PricePerToken.tsx: -------------------------------------------------------------------------------- 1 | import cx from "classnames"; 2 | import { AiOutlineSwap } from "react-icons/ai"; 3 | 4 | import { usePricePerToken } from "../hooks/usePricePerToken"; 5 | import { useSwap } from "../hooks/useSwap"; 6 | 7 | import { Button, SkeletonLoader } from "~/systems/UI"; 8 | 9 | const style = { 10 | wrapper: `flex items-center gap-3 my-4 px-2 text-sm text-gray-400`, 11 | priceContainer: `min-w-[150px] cursor-pointer`, 12 | }; 13 | 14 | const ToPriceLoading = () => ( 15 | 20 | 21 | 22 | ); 23 | 24 | export function PricePerToken() { 25 | const data = usePricePerToken(); 26 | const { state } = useSwap(); 27 | 28 | return ( 29 |
34 |
35 | 1 {data.assetFrom.symbol} ={" "} 36 | 37 | {state.isLoading ? : <>{data.pricePerToken} } 38 | 39 | {data.assetTo.symbol} 40 |
41 | 49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /packages/app/src/systems/Swap/components/SwapPreview.tsx: -------------------------------------------------------------------------------- 1 | import { BsArrowDown } from "react-icons/bs"; 2 | 3 | import { useSwap } from "../hooks/useSwap"; 4 | import { useSwapPreview } from "../hooks/useSwapPreview"; 5 | import { SwapDirection } from "../types"; 6 | 7 | import { 8 | PreviewItem, 9 | PreviewTable, 10 | NetworkFeePreviewItem, 11 | } from "~/systems/Core"; 12 | 13 | export function SwapPreview() { 14 | const { state } = useSwap(); 15 | const preview = useSwapPreview(); 16 | 17 | const { slippage } = preview; 18 | const { coinTo, coinFrom, direction, txCost, isLoading } = state; 19 | 20 | const isFrom = direction === SwapDirection.fromTo; 21 | const inputSymbol = isFrom ? coinTo?.symbol : coinFrom?.symbol; 22 | const inputText = isFrom 23 | ? "Min. received after slippage" 24 | : "Max. sent after slippage"; 25 | 26 | return ( 27 |
28 |
29 | 30 |
31 | 32 | 37 | 42 | 47 | 48 | 49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /packages/app/src/systems/Swap/components/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./PricePerToken"; 2 | export * from "./SwapPreview"; 3 | export * from "./SwapWidget"; 4 | -------------------------------------------------------------------------------- /packages/app/src/systems/Swap/hooks/usePricePerToken.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "@xstate/react"; 2 | import { bn } from "fuels"; 3 | import { useState } from "react"; 4 | 5 | import type { SwapMachineState } from "../machines/swapMachine"; 6 | import { SwapDirection } from "../types"; 7 | import { getPricePerToken } from "../utils"; 8 | 9 | import { useSwapContext } from "./useSwap"; 10 | 11 | import { compareStates } from "~/systems/Core"; 12 | 13 | const selectors = { 14 | coinFrom: ({ context: ctx }: SwapMachineState) => { 15 | return { 16 | symbol: ctx.coinFrom?.symbol || "", 17 | amount: bn(ctx.fromAmount?.raw), 18 | }; 19 | }, 20 | coinTo: ({ context: ctx }: SwapMachineState) => { 21 | return { 22 | symbol: ctx.coinTo?.symbol || "", 23 | amount: bn(ctx.toAmount?.raw), 24 | }; 25 | }, 26 | isFrom: ({ context: ctx }: SwapMachineState) => { 27 | return ctx.direction === SwapDirection.fromTo; 28 | }, 29 | }; 30 | 31 | export function usePricePerToken() { 32 | const { service } = useSwapContext(); 33 | const coinFrom = useSelector(service, selectors.coinFrom, compareStates); 34 | const coinTo = useSelector(service, selectors.coinTo, compareStates); 35 | const isFrom = useSelector(service, selectors.isFrom); 36 | const assets = isFrom ? [coinFrom, coinTo] : [coinTo, coinFrom]; 37 | 38 | const [revert, setRevert] = useState(false); 39 | const assetFrom = revert ? assets[1] : assets[0]; 40 | const assetTo = revert ? assets[0] : assets[1]; 41 | const pricePerToken = getPricePerToken(assetFrom.amount, assetTo.amount); 42 | 43 | function onToggleAssets() { 44 | setRevert((s) => !s); 45 | } 46 | 47 | return { 48 | pricePerToken, 49 | onToggleAssets, 50 | assetFrom, 51 | assetTo, 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /packages/app/src/systems/Swap/hooks/useSwapCoinSelector.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "@xstate/react"; 2 | import { useMemo } from "react"; 3 | 4 | import type { SwapMachineState } from "../machines/swapMachine"; 5 | import { SwapDirection } from "../types"; 6 | 7 | import { useSwapContext } from "./useSwap"; 8 | 9 | import type { CoinSelectorProps } from "~/systems/Core"; 10 | import { isCoinEth } from "~/systems/Core"; 11 | import type { Coin } from "~/types"; 12 | 13 | const selectors = { 14 | coinFrom: (state: SwapMachineState) => { 15 | return state.context.coinFrom; 16 | }, 17 | coinTo: (state: SwapMachineState) => { 18 | return state.context.coinTo; 19 | }, 20 | }; 21 | 22 | export function useSwapCoinSelector( 23 | direction: SwapDirection 24 | ): CoinSelectorProps { 25 | const { service, send } = useSwapContext(); 26 | const isFrom = direction === SwapDirection.fromTo; 27 | const coinSelector = isFrom ? selectors.coinFrom : selectors.coinTo; 28 | const coin = useSelector(service, coinSelector); 29 | const isETH = useMemo(() => isCoinEth(coin), [coin?.assetId]); 30 | 31 | function handleSelectCoin(item: Coin) { 32 | send("SELECT_COIN", { data: { direction, coin: item } }); 33 | } 34 | 35 | return { 36 | coin, 37 | onChange: handleSelectCoin, 38 | ...(isETH && { 39 | isReadOnly: true, 40 | tooltip: "Currently, we only support ETH <-> TOKEN.", 41 | }), 42 | } as CoinSelectorProps; 43 | } 44 | -------------------------------------------------------------------------------- /packages/app/src/systems/Swap/hooks/useSwapGlobalState.tsx: -------------------------------------------------------------------------------- 1 | import { atom, useAtom } from "jotai"; 2 | 3 | import type { SwapMachineContext } from "../types"; 4 | 5 | const swapGlobalAtom = atom>({}); 6 | 7 | export function useSwapGlobalState() { 8 | return useAtom(swapGlobalAtom); 9 | } 10 | -------------------------------------------------------------------------------- /packages/app/src/systems/Swap/hooks/useSwapPreview.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "@xstate/react"; 2 | import { bn, format } from "fuels"; 3 | 4 | import type { SwapMachineState } from "../machines/swapMachine"; 5 | import { SwapDirection } from "../types"; 6 | import { calculatePriceImpact, calculatePriceWithSlippage } from "../utils"; 7 | 8 | import { useSwapContext } from "./useSwap"; 9 | 10 | import { useSlippage } from "~/systems/Core"; 11 | 12 | const selectors = { 13 | hasPreview: (state: SwapMachineState) => { 14 | return !state.hasTag("loading") && state.context?.previewInfo; 15 | }, 16 | outputAmount: (state: SwapMachineState) => { 17 | const ctx = state.context; 18 | const amount = bn(ctx?.toAmount?.raw); 19 | return format(amount); 20 | }, 21 | inputAmount: (state: SwapMachineState) => { 22 | const ctx = state.context; 23 | const isFrom = ctx?.direction === SwapDirection.fromTo; 24 | const amount = bn((isFrom ? ctx?.toAmount : ctx?.fromAmount)?.raw); 25 | const price = calculatePriceWithSlippage( 26 | amount, 27 | ctx?.direction, 28 | ctx?.slippage || 0 29 | ); 30 | return format(price); 31 | }, 32 | priceImpact: (state: SwapMachineState) => { 33 | const ctx = state.context; 34 | return ctx && calculatePriceImpact(ctx); 35 | }, 36 | }; 37 | 38 | export function useSwapPreview() { 39 | const { service } = useSwapContext(); 40 | const hasPreview = useSelector(service, selectors.hasPreview); 41 | const outputAmount = useSelector(service, selectors.outputAmount); 42 | const inputAmount = useSelector(service, selectors.inputAmount); 43 | const priceImpact = useSelector(service, selectors.priceImpact); 44 | const slippage = useSlippage(); 45 | 46 | return { 47 | hasPreview, 48 | outputAmount, 49 | inputAmount, 50 | priceImpact, 51 | slippage, 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /packages/app/src/systems/Swap/hooks/useSwapURLParams.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { useSearchParams } from "react-router-dom"; 3 | 4 | import type { SwapMachineContext } from "../types"; 5 | import { SwapDirection } from "../types"; 6 | 7 | import { useSwapGlobalState } from "./useSwapGlobalState"; 8 | 9 | import { TOKENS, ETH, DAI } from "~/systems/Core"; 10 | import type { Maybe } from "~/types"; 11 | 12 | function findCoin(dir: Maybe) { 13 | return useMemo(() => { 14 | return dir && TOKENS.find((t) => t.assetId === dir || t.symbol === dir); 15 | }, [dir]); 16 | } 17 | 18 | /** 19 | * TODO: this method in future can generate bugs since Coin.symbol 20 | * isn't something unique. 21 | */ 22 | function getParamsByContext(ctx: Partial) { 23 | if (!ctx.coinFrom && !ctx.coinTo) { 24 | return { from: ETH.symbol, to: DAI.symbol }; 25 | } 26 | return { 27 | ...(ctx.coinFrom && { from: ctx.coinFrom.symbol }), 28 | ...(ctx.coinTo && { to: ctx.coinTo.symbol }), 29 | }; 30 | } 31 | 32 | export function useSwapURLParams() { 33 | const [globalState] = useSwapGlobalState(); 34 | const [query, setQuery] = useSearchParams(getParamsByContext(globalState)); 35 | const from = query.get(SwapDirection.fromTo); 36 | const to = query.get(SwapDirection.toFrom); 37 | const coinFrom = findCoin(from); 38 | const coinTo = findCoin(to); 39 | 40 | function setCoinParams(ctx: SwapMachineContext) { 41 | setQuery(getParamsByContext(ctx), { replace: true }); 42 | } 43 | 44 | return { 45 | coinFrom, 46 | coinTo, 47 | setCoinParams, 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /packages/app/src/systems/Swap/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./routes"; 2 | -------------------------------------------------------------------------------- /packages/app/src/systems/Swap/pages/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./SwapPage"; 2 | -------------------------------------------------------------------------------- /packages/app/src/systems/Swap/routes.tsx: -------------------------------------------------------------------------------- 1 | import { Route } from "react-router-dom"; 2 | 3 | import { PrivateRoute } from "../Core"; 4 | 5 | import { SwapProvider } from "./hooks/useSwap"; 6 | import { SwapPage } from "./pages"; 7 | 8 | import { Pages } from "~/types"; 9 | 10 | export const swapRoutes = ( 11 | 15 | 16 | 17 | 18 | 19 | } 20 | /> 21 | ); 22 | -------------------------------------------------------------------------------- /packages/app/src/systems/Swap/state.ts: -------------------------------------------------------------------------------- 1 | import { atom, useAtomValue, useSetAtom } from 'jotai'; 2 | import { useRef } from 'react'; 3 | 4 | import { SwapDirection } from './types'; 5 | 6 | import { TOKENS } from '~/systems/Core'; 7 | import type { Coin, Maybe } from '~/types'; 8 | 9 | export const swapActiveInputAtom = atom(SwapDirection.fromTo); 10 | export const swapCoinsAtom = atom<[Coin, Coin]>([TOKENS[0], TOKENS[1]]); 11 | export const swapIsTypingAtom = atom(false); 12 | 13 | export const useValueIsTyping = () => useAtomValue(swapIsTypingAtom); 14 | export const useSetIsTyping = () => { 15 | const setTyping = useSetAtom(swapIsTypingAtom); 16 | const timeout = useRef(0); 17 | 18 | return (typing: boolean) => { 19 | setTyping(typing); 20 | if (typing) { 21 | clearTimeout(timeout.current); 22 | timeout.current = Number( 23 | setTimeout(() => { 24 | setTyping(false); 25 | }, 600) 26 | ); 27 | } 28 | }; 29 | }; 30 | 31 | export const swapHasSwappedAtom = atom(false); 32 | export const swapAmountAtom = atom>(null); 33 | -------------------------------------------------------------------------------- /packages/app/src/systems/Swap/types.ts: -------------------------------------------------------------------------------- 1 | import type { BN, CoinQuantity, WalletLocked } from 'fuels'; 2 | import type { QueryClient } from 'react-query'; 3 | 4 | import type { TransactionCost } from '../Core/utils/gas'; 5 | 6 | import type { Coin, Maybe } from '~/types'; 7 | import type { 8 | ExchangeContractAbi, 9 | PoolInfoOutput, 10 | PreviewInfoOutput, 11 | } from '~/types/contracts/ExchangeContractAbi'; 12 | 13 | export type CoinAmount = { 14 | raw: BN; 15 | value: string; 16 | }; 17 | 18 | export enum SwapDirection { 19 | 'fromTo' = 'from', 20 | 'toFrom' = 'to', 21 | } 22 | 23 | export type SwapMachineContext = { 24 | client?: QueryClient; 25 | wallet?: Maybe; 26 | contract: Maybe; 27 | balances?: Maybe; 28 | direction: SwapDirection; 29 | coinFrom?: Coin; 30 | coinTo?: Coin; 31 | coinFromBalance?: Maybe; 32 | coinToBalance?: Maybe; 33 | ethBalance?: Maybe; 34 | amountPlusSlippage?: Maybe; 35 | amountLessSlippage?: Maybe; 36 | fromAmount?: Maybe; 37 | toAmount?: Maybe; 38 | poolInfo?: Maybe; 39 | txCost?: TransactionCost; 40 | slippage?: number; 41 | previewInfo?: Maybe; 42 | }; 43 | -------------------------------------------------------------------------------- /packages/app/src/systems/Swap/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './helpers'; 2 | export * from './queries'; 3 | -------------------------------------------------------------------------------- /packages/app/src/systems/UI/components/ButtonGroup.tsx: -------------------------------------------------------------------------------- 1 | import { FocusScope, useFocusManager } from "@react-aria/focus"; 2 | import { mergeProps } from "@react-aria/utils"; 3 | import type { ReactNode, KeyboardEvent } from "react"; 4 | import { Children, cloneElement } from "react"; 5 | 6 | function ButtonGroupChildren({ children }: ButtonGroupProps) { 7 | const focusManager = useFocusManager(); 8 | const onKeyDown = (e: KeyboardEvent) => { 9 | // eslint-disable-next-line default-case 10 | switch (e.key) { 11 | case "ArrowRight": 12 | focusManager.focusNext({ wrap: true }); 13 | break; 14 | case "ArrowLeft": 15 | focusManager.focusPrevious({ wrap: true }); 16 | break; 17 | } 18 | }; 19 | 20 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 21 | const customChildren = Children.toArray(children).map((child: any) => 22 | cloneElement(child, mergeProps(child.props, { onKeyDown })) 23 | ); 24 | 25 | return <>{customChildren}; 26 | } 27 | 28 | type ButtonGroupProps = { 29 | children: ReactNode; 30 | }; 31 | 32 | export function ButtonGroup({ children }: ButtonGroupProps) { 33 | return ( 34 | 35 | {children} 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /packages/app/src/systems/UI/components/Card.tsx: -------------------------------------------------------------------------------- 1 | import cx from "classnames"; 2 | import type { FC, ReactNode } from "react"; 3 | import { Children } from "react"; 4 | 5 | export type CardProps = { 6 | children: ReactNode; 7 | className?: string; 8 | withWrapper?: boolean; 9 | }; 10 | 11 | export type CardTitleProps = { 12 | children: ReactNode; 13 | elementRight?: ReactNode; 14 | }; 15 | 16 | type CardComponent = FC & { 17 | Title: FC; 18 | }; 19 | 20 | export const Card: CardComponent = ({ className, children }) => { 21 | const title = Children.toArray(children)?.find( 22 | (child: any) => child.type?.id === "CardTitle" // eslint-disable-line @typescript-eslint/no-explicit-any 23 | ); 24 | const customChildren = Children.toArray(children)?.filter( 25 | (child: any) => child.type?.id !== "CardTitle" // eslint-disable-line @typescript-eslint/no-explicit-any 26 | ); 27 | 28 | return ( 29 |
30 | {title} 31 | {title &&
} 32 |
{customChildren}
33 |
34 | ); 35 | }; 36 | 37 | const CardTitle: FC & { id: string } = ({ 38 | children, 39 | elementRight, 40 | }) => ( 41 |
42 |

{children}

43 | {elementRight} 44 |
45 | ); 46 | 47 | CardTitle.id = "CardTitle"; 48 | Card.Title = CardTitle; 49 | -------------------------------------------------------------------------------- /packages/app/src/systems/UI/components/Input.tsx: -------------------------------------------------------------------------------- 1 | import type { AriaTextFieldOptions } from "@react-aria/textfield"; 2 | import { useTextField } from "@react-aria/textfield"; 3 | import { mergeRefs } from "@react-aria/utils"; 4 | import cx from "classnames"; 5 | import type { FC } from "react"; 6 | import { forwardRef, useRef } from "react"; 7 | 8 | type InputProps = AriaTextFieldOptions<"input"> & { 9 | className?: string; 10 | }; 11 | 12 | export const Input: FC = forwardRef( 13 | ({ className, ...props }, ref) => { 14 | const innerRef = useRef(null); 15 | const { inputProps } = useTextField(props, innerRef); 16 | return ( 17 | 24 | ); 25 | } 26 | ); 27 | -------------------------------------------------------------------------------- /packages/app/src/systems/UI/components/InvertButton.tsx: -------------------------------------------------------------------------------- 1 | import cx from "classnames"; 2 | import { useState } from "react"; 3 | import { BsArrowDown, BsArrowUp } from "react-icons/bs"; 4 | 5 | import { Button } from "./Button"; 6 | 7 | const style = { 8 | confirmButton: ` 9 | p-0 relative w-10 h-10 rounded-xl mb-3 mt-0 translate-x-[60px] 10 | border-2 border-gray-700 bg-gray-800 cursor-pointer text-gray-400 11 | sm:translate-x-0 sm:my-1 sm:w-12 sm:h-12 sm:rounded-xl hover:text-gray-50 12 | disabled:hover:text-gray-400 disabled:opacity-90`, 13 | icon: `transition-all w-[14px] sm:w-[18px]`, 14 | iconLeft: `translate-x-[6px]`, 15 | iconRight: `translate-x-[-6px]`, 16 | rotate: `rotate-180`, 17 | }; 18 | 19 | type InvertButtonProps = { 20 | isDisabled?: boolean; 21 | onClick: () => void; 22 | }; 23 | 24 | export function InvertButton({ onClick, isDisabled }: InvertButtonProps) { 25 | const [isInverted, setInverted] = useState(false); 26 | 27 | function handleClick() { 28 | setInverted((s) => !s); 29 | onClick?.(); 30 | } 31 | 32 | return ( 33 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /packages/app/src/systems/UI/components/Link.tsx: -------------------------------------------------------------------------------- 1 | import { useButton } from "@react-aria/button"; 2 | import type { AriaButtonProps } from "@react-types/button"; 3 | import cx from "classnames"; 4 | import type { ReactNode } from "react"; 5 | import { useRef } from "react"; 6 | 7 | export type LinkProps = AriaButtonProps<"a"> & { 8 | href?: string; 9 | children: ReactNode; 10 | isExternal?: boolean; 11 | className?: string; 12 | }; 13 | 14 | export function Link({ 15 | href, 16 | isExternal, 17 | className, 18 | children, 19 | ...props 20 | }: LinkProps) { 21 | const ref = useRef(null); 22 | const { buttonProps, isPressed } = useButton( 23 | { ...props, elementType: "a" }, 24 | ref 25 | ); 26 | 27 | return ( 28 | 36 | {children} 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /packages/app/src/systems/UI/components/NumberInput.tsx: -------------------------------------------------------------------------------- 1 | import cx from "classnames"; 2 | import type { NumberFormatProps } from "react-number-format"; 3 | import NumberFormat from "react-number-format"; 4 | 5 | import type { Maybe } from "~/types"; 6 | 7 | const style = { 8 | transferPropContainer: `bg-gray-700 rounded-xl px-4 py-2 border border-gray-700 flex justify-between`, 9 | transferPropInput: `bg-transparent placeholder:text-gray-300 outline-none w-full`, 10 | }; 11 | 12 | export type NumberInputProps = Omit & { 13 | disabled?: boolean; 14 | value?: number | Maybe; 15 | onChange?: (value: string) => void; 16 | className?: string; 17 | }; 18 | 19 | export function NumberInput({ 20 | onChange, 21 | disabled, 22 | className, 23 | placeholder = "0.0", 24 | thousandSeparator = false, 25 | ...props 26 | }: NumberInputProps) { 27 | return ( 28 |
29 | onChange?.(e.value)} 35 | className={style.transferPropInput} 36 | /> 37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /packages/app/src/systems/UI/components/SkeletonLoader.tsx: -------------------------------------------------------------------------------- 1 | import cx from "classnames"; 2 | import type { IContentLoaderProps } from "react-content-loader"; 3 | import ContentLoader from "react-content-loader"; 4 | 5 | export const SkeletonLoader: React.FC = ({ 6 | children, 7 | ...props 8 | }) => ( 9 | 16 | {children} 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /packages/app/src/systems/UI/components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import type { SpinnerCircularProps } from "spinners-react/lib/cjs/SpinnerCircular"; 2 | import { SpinnerCircular } from "spinners-react/lib/cjs/SpinnerCircular"; 3 | 4 | const VARIANTS = { 5 | base: { 6 | color: "rgba(255,255,255,0.7)", 7 | secondaryColor: "rgba(255,255,255,0.2)", 8 | }, 9 | primary: { 10 | color: "#2aac98", 11 | secondaryColor: "#134034", 12 | }, 13 | }; 14 | 15 | export type SpinnerProps = SpinnerCircularProps & { 16 | variant?: "primary" | "base"; 17 | }; 18 | 19 | export function Spinner({ variant = "primary", ...props }: SpinnerProps) { 20 | return ; 21 | } 22 | 23 | Spinner.defaultProps = { 24 | size: 22, 25 | thickness: 250, 26 | speed: 150, 27 | variant: "primary", 28 | }; 29 | -------------------------------------------------------------------------------- /packages/app/src/systems/UI/components/Toaster.tsx: -------------------------------------------------------------------------------- 1 | import toast, { ToastBar, Toaster as Root } from "react-hot-toast"; 2 | import { MdClose } from "react-icons/md"; 3 | 4 | import { Button } from "./Button"; 5 | 6 | export function Toaster() { 7 | return ( 8 | 9 | {(t) => ( 10 | 14 | {({ icon, message }) => ( 15 |
16 | {icon} 17 | {message} 18 | {t.type !== "loading" && ( 19 | 26 | )} 27 |
28 | )} 29 |
30 | )} 31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /packages/app/src/systems/UI/components/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import * as RTooltip from "@radix-ui/react-tooltip"; 2 | import cx from "classnames"; 3 | import type { FC, ReactNode } from "react"; 4 | 5 | export type TooltipProps = RTooltip.TooltipProps & { 6 | content: ReactNode; 7 | side?: RTooltip.PopperContentProps["side"]; 8 | align?: RTooltip.PopperContentProps["align"]; 9 | className?: string; 10 | arrowClassName?: string; 11 | contentClassName?: string; 12 | sideOffset?: RTooltip.TooltipContentProps["sideOffset"]; 13 | alignOffset?: RTooltip.TooltipContentProps["alignOffset"]; 14 | }; 15 | 16 | export const Tooltip: FC = ({ 17 | children, 18 | content, 19 | side = "top", 20 | align, 21 | className, 22 | arrowClassName, 23 | contentClassName, 24 | sideOffset, 25 | alignOffset, 26 | ...props 27 | }) => ( 28 | 29 | 30 | {children} 31 | 38 | 44 |
45 | {content} 46 |
47 |
48 |
49 |
50 | ); 51 | -------------------------------------------------------------------------------- /packages/app/src/systems/UI/components/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./Accordion"; 2 | export * from "./Button"; 3 | export * from "./ButtonGroup"; 4 | export * from "./Card"; 5 | export * from "./Dialog"; 6 | export * from "./Input"; 7 | export * from "./InvertButton"; 8 | export * from "./Link"; 9 | export * from "./Menu"; 10 | export * from "./NumberInput"; 11 | export * from "./Popover"; 12 | export * from "./SkeletonLoader"; 13 | export * from "./Spinner"; 14 | export * from "./Toaster"; 15 | export * from "./Tooltip"; 16 | -------------------------------------------------------------------------------- /packages/app/src/systems/UI/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./components"; 2 | -------------------------------------------------------------------------------- /packages/app/src/systems/Welcome/components/AddAssets.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@fuel-ui/react"; 2 | 3 | import { useWelcomeSteps } from "../hooks"; 4 | 5 | import { WelcomeImage } from "./WelcomeImage"; 6 | import { WelcomeStep } from "./WelcomeStep"; 7 | 8 | export function AddAssets() { 9 | const { send, state } = useWelcomeSteps(); 10 | 11 | function handleAddAssets() { 12 | send("ADD_ASSETS"); 13 | } 14 | 15 | return ( 16 | 17 | 18 |

Add SwaySwap assets

19 |

20 | To see the SwaySwap assets in your wallet we need to add them. 21 |
22 | Click “Add Assets” below and approve it. 23 |

24 | 31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /packages/app/src/systems/Welcome/components/AddFunds.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "@xstate/react"; 2 | 3 | import { useWelcomeSteps } from "../hooks"; 4 | 5 | import { WelcomeImage } from "./WelcomeImage"; 6 | import { WelcomeStep } from "./WelcomeStep"; 7 | 8 | import { FaucetApp } from "~/systems/Faucet"; 9 | 10 | export function AddFunds() { 11 | const { next, service } = useWelcomeSteps(); 12 | const isFetchingBalance = useSelector(service, (args) => 13 | args.matches("fecthingBalance") 14 | ); 15 | 16 | return ( 17 | 18 | 19 |

Add some test ETH to your wallet

20 |

21 | To get started you'll need some funds. 22 |
23 | Click “Give me ETH” below to get some from the faucet. 24 |

25 | 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /packages/app/src/systems/Welcome/components/MintAssets.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@fuel-ui/react"; 2 | 3 | import { useWelcomeSteps } from "../hooks"; 4 | 5 | import { WelcomeImage } from "./WelcomeImage"; 6 | import { WelcomeStep } from "./WelcomeStep"; 7 | 8 | export function MintAssets() { 9 | const { send, state } = useWelcomeSteps(); 10 | 11 | function handleAddAssets() { 12 | send("MINT_ASSETS"); 13 | } 14 | 15 | return ( 16 | 17 | 18 |

Add some test Assets to your wallet

19 |

20 | To get started you'll need some funds. 21 |
22 | Click “Mint Assets” below to mint some test sETH and DAI 23 | tokens that are the tokens used on SwaySwap. 24 |

25 | 32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /packages/app/src/systems/Welcome/components/StepsIndicator.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "@xstate/react"; 2 | import cx from "classnames"; 3 | 4 | import type { Step } from "../hooks"; 5 | import { stepsSelectors, useWelcomeSteps } from "../hooks"; 6 | 7 | function getClasses(id: number, current: Step) { 8 | return cx({ done: current.id > id, active: current.id === id }); 9 | } 10 | 11 | export function StepsIndicator() { 12 | const { service } = useWelcomeSteps(); 13 | const current = useSelector(service, stepsSelectors.current); 14 | 15 | return ( 16 |
    17 |
  • 18 | Connect Wallet 19 |
  • 20 |
  • 21 | Faucet 22 |
  • 23 |
  • 24 | Add Assets 25 |
  • 26 |
  • 27 | Mint Assets 28 |
  • 29 |
  • 30 | Done 31 |
  • 32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /packages/app/src/systems/Welcome/components/WelcomeConnect.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@fuel-ui/react"; 2 | import { useSelector } from "@xstate/react"; 3 | 4 | import { useWelcomeSteps } from "../hooks"; 5 | 6 | import { WelcomeImage } from "./WelcomeImage"; 7 | import { WelcomeStep } from "./WelcomeStep"; 8 | 9 | export const WelcomeConnect = () => { 10 | const { service, send } = useWelcomeSteps(); 11 | const installWallet = useSelector(service, (s) => s.matches("installWallet")); 12 | 13 | return ( 14 | 15 | 16 |

Welcome to SwaySwap

17 | {installWallet ? ( 18 | <> 19 |

20 | To get started you'll need to install 21 |
the Fuel wallet. Click the button below to learn how 22 | to install. After you have installed come back to this page. 23 |

24 | 29 | 32 | 33 | 34 | ) : ( 35 | <> 36 |

37 | The wallet is installed! 38 |
39 | Click the button below to connect your wallet to SwaySwap. 40 |

41 | 49 | 50 | )} 51 |
52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /packages/app/src/systems/Welcome/components/WelcomeImage.tsx: -------------------------------------------------------------------------------- 1 | import { relativeUrl } from "~/systems/Core"; 2 | 3 | export function WelcomeImage({ src }: { src: string }) { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /packages/app/src/systems/Welcome/components/WelcomeNavItem.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "@xstate/react"; 2 | import cx from "classnames"; 3 | import { motion } from "framer-motion"; 4 | 5 | import { useWelcomeSteps, stepsSelectors } from "../hooks"; 6 | 7 | type SidebarItemProps = { 8 | id: number; 9 | label: string; 10 | }; 11 | 12 | export function WelcomeNavItem({ id, label }: SidebarItemProps) { 13 | const { service } = useWelcomeSteps(); 14 | const current = useSelector(service, stepsSelectors.current); 15 | const isActive = id === current?.id; 16 | const isDone = id < current?.id; 17 | const isDisabled = id > current?.id; 18 | 19 | return ( 20 |
29 | 30 | {label} 31 | 48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /packages/app/src/systems/Welcome/components/WelcomeSidebar.tsx: -------------------------------------------------------------------------------- 1 | import { WelcomeNavItem } from "./WelcomeNavItem"; 2 | import { WelcomeSidebarBullet } from "./WelcomeSidebarBullet"; 3 | 4 | import { relativeUrl } from "~/systems/Core"; 5 | 6 | export function WelcomeSidebar() { 7 | return ( 8 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /packages/app/src/systems/Welcome/components/WelcomeSidebarBullet.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "@xstate/react"; 2 | import { motion } from "framer-motion"; 3 | 4 | import { useWelcomeSteps, stepsSelectors } from "../hooks"; 5 | 6 | export function WelcomeSidebarBullet() { 7 | const { service } = useWelcomeSteps(); 8 | const current = useSelector(service, stepsSelectors.current); 9 | 10 | function getVariant() { 11 | if (current.id === 0) return "first"; 12 | if (current.id === 1) return "second"; 13 | if (current.id === 2) return "third"; 14 | if (current.id === 3) return "fourth"; 15 | return "fifth"; 16 | } 17 | 18 | return ( 19 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /packages/app/src/systems/Welcome/components/WelcomeStep.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | 3 | import { AnimatedPage } from "~/systems/Core"; 4 | 5 | type WelcomeStepProps = { 6 | children: ReactNode; 7 | }; 8 | 9 | export function WelcomeStep({ children }: WelcomeStepProps) { 10 | return ( 11 | 12 |
{children}
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /packages/app/src/systems/Welcome/components/WelcomeTerms.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | import { useWelcomeSteps } from "../hooks"; 4 | 5 | import { WelcomeImage } from "./WelcomeImage"; 6 | import { WelcomeStep } from "./WelcomeStep"; 7 | 8 | import { Button, Link } from "~/systems/UI"; 9 | 10 | const DISCLAIMER_URL = 11 | "https://github.com/FuelLabs/swayswap/blob/master/docs/LEGAL_DISCLAIMER.md"; 12 | 13 | export function WelcomeTerms() { 14 | const { send } = useWelcomeSteps(); 15 | const [agreement, setAgreement] = useState(false); 16 | 17 | function handleDone() { 18 | send("ACCEPT_AGREEMENT"); 19 | } 20 | 21 | return ( 22 | 23 | 24 |

25 | This is running on the Fuel test network. No real funds are used. 26 | Demonstration purposes only. 27 |

28 |
29 | 46 |
47 | 56 |
57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /packages/app/src/systems/Welcome/components/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./AddAssets"; 2 | export * from "./AddFunds"; 3 | export * from "./StepsIndicator"; 4 | export * from "./WelcomeTerms"; 5 | export * from "./WelcomeNavItem"; 6 | export * from "./WelcomeSidebar"; 7 | export * from "./WelcomeSidebarBullet"; 8 | export * from "./WelcomeStep"; 9 | export * from "./WelcomeConnect"; 10 | -------------------------------------------------------------------------------- /packages/app/src/systems/Welcome/hooks/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./useWelcomeSteps"; 2 | -------------------------------------------------------------------------------- /packages/app/src/systems/Welcome/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./routes"; 2 | export * from "./hooks"; 3 | -------------------------------------------------------------------------------- /packages/app/src/systems/Welcome/machines/index.ts: -------------------------------------------------------------------------------- 1 | export * from './welcomeMachine'; 2 | -------------------------------------------------------------------------------- /packages/app/src/systems/Welcome/pages/WelcomePage.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet, Route, Routes, useLocation } from "react-router-dom"; 2 | 3 | import { 4 | WelcomeSidebar, 5 | WelcomeTerms, 6 | StepsIndicator, 7 | AddAssets, 8 | AddFunds, 9 | } from "../components"; 10 | import { MintAssets } from "../components/MintAssets"; 11 | import { WelcomeConnect } from "../components/WelcomeConnect"; 12 | 13 | import { useBreakpoint } from "~/systems/Core"; 14 | import { Pages } from "~/types"; 15 | 16 | export function WelcomePage() { 17 | const location = useLocation(); 18 | const breakpoint = useBreakpoint(); 19 | 20 | return ( 21 |
22 | {breakpoint === "lg" && } 23 |
24 | 25 | } /> 26 | } /> 27 | } /> 28 | } /> 29 | } /> 30 | 31 | 32 | 33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /packages/app/src/systems/Welcome/pages/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./WelcomePage"; 2 | -------------------------------------------------------------------------------- /packages/app/src/systems/Welcome/routes.tsx: -------------------------------------------------------------------------------- 1 | import { Route } from "react-router-dom"; 2 | 3 | import { WelcomeStepsProvider } from "./hooks"; 4 | import { WelcomePage } from "./pages"; 5 | 6 | import { Pages } from "~/types"; 7 | 8 | export const welcomeRoutes = ( 9 | 13 | 14 | 15 | } 16 | /> 17 | ); 18 | -------------------------------------------------------------------------------- /packages/app/src/types/contracts/common.d.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | 3 | /* tslint:disable */ 4 | /* eslint-disable */ 5 | 6 | /* 7 | Fuels version: 0.35.0 8 | Forc version: 0.35.3 9 | Fuel-Core version: 0.17.3 10 | */ 11 | 12 | /* 13 | Mimics Sway Enum, requires at least one Key-Value but 14 | does not raise error on multiple pairs. 15 | This is done in the abi-coder 16 | */ 17 | export type Enum }> = Partial & U[keyof U]; 18 | 19 | /* 20 | Mimics Sway Option and Vectors. 21 | Vectors are treated like arrays in Typescript. 22 | */ 23 | export type Option = T | undefined; 24 | 25 | export type Vec = T[]; 26 | -------------------------------------------------------------------------------- /packages/app/src/types/contracts/index.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | 3 | /* tslint:disable */ 4 | /* eslint-disable */ 5 | 6 | /* 7 | Fuels version: 0.35.0 8 | Forc version: 0.35.3 9 | Fuel-Core version: 0.17.3 10 | */ 11 | 12 | export type { ExchangeContractAbi } from './ExchangeContractAbi'; 13 | export type { TokenContractAbi } from './TokenContractAbi'; 14 | 15 | export { ExchangeContractAbi__factory } from './factories/ExchangeContractAbi__factory'; 16 | export { TokenContractAbi__factory } from './factories/TokenContractAbi__factory'; 17 | -------------------------------------------------------------------------------- /packages/app/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export type Maybe = T | null | undefined; 2 | 3 | export interface Coin { 4 | assetId: string; 5 | symbol?: string; 6 | name?: string; 7 | img?: string; 8 | pairOf?: Coin[]; 9 | } 10 | 11 | export enum Pages { 12 | 'home' = '/', 13 | 'swap' = '/swap', 14 | 'pool' = '/pool', 15 | 'pool.list' = 'list', 16 | 'pool.addLiquidity' = 'add-liquidity', 17 | 'pool.removeLiquidity' = 'remove-liquidity', 18 | 'welcome' = '/welcome', 19 | 'welcomeInstall' = 'install', 20 | 'welcomeConnect' = 'connect', 21 | 'welcomeFaucet' = 'faucet', 22 | 'welcomeTerms' = 'terms', 23 | 'welcomeAddAssets' = 'add-assets', 24 | 'welcomeMint' = 'mint', 25 | } 26 | 27 | export enum Queries { 28 | UserQueryBalances = 'UserQueryBalances', 29 | FaucetQuery = 'FaucetQuery', 30 | } 31 | 32 | export enum AppEvents { 33 | 'refetchBalances' = 'refetchBalances', 34 | 'updatedBalances' = 'updatedBalances', 35 | } 36 | -------------------------------------------------------------------------------- /packages/app/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/app/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const colors = require('tailwindcss/colors'); 2 | 3 | delete colors['lightBlue']; 4 | delete colors['warmGray']; 5 | delete colors['trueGray']; 6 | delete colors['coolGray']; 7 | delete colors['blueGray']; 8 | 9 | module.exports = { 10 | content: ['./src/**/*.{js,ts,jsx,tsx}'], 11 | theme: { 12 | extend: {}, 13 | screens: { 14 | sm: '640px', 15 | md: '960px', 16 | lg: '1440px', 17 | }, 18 | colors: { 19 | ...colors, 20 | gray: { 21 | 50: '#E8EAED', 22 | 100: '#D4D8DD', 23 | 200: '#AAB1BB', 24 | 300: '#7C8897', 25 | 400: '#58626F', 26 | 500: '#363C44', 27 | 600: '#2B3036', 28 | 700: '#202328', 29 | 800: '#17191C', 30 | 900: '#0B0D0E', 31 | }, 32 | primary: { 33 | 50: '#eff6f5', 34 | 100: '#d2eff2', 35 | 200: '#9fe5e2', 36 | 300: '#66cbc0', 37 | 400: '#2aac98', 38 | 500: '#1e9071', 39 | 600: '#1b7958', 40 | 700: '#195d46', 41 | 800: '#134034', 42 | 900: '#0d2727', 43 | }, 44 | }, 45 | fontFamily: { 46 | sans: ['InterVariable', 'sans-serif'], 47 | display: ['RalewayVariable', 'sans-serif'], 48 | }, 49 | }, 50 | variants: { 51 | extend: { 52 | backgroundColor: ['disabled'], 53 | textColor: ['disabled'], 54 | scale: ['disabled'], 55 | }, 56 | }, 57 | plugins: [require('@tailwindcss/typography')], 58 | }; 59 | -------------------------------------------------------------------------------- /packages/app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "baseUrl": ".", 6 | "rootDir": ".", 7 | "paths": { 8 | "~/*": ["./src/*"] 9 | }, 10 | "types": ["@fuel-wallet/sdk", "jest", "chrome"] 11 | }, 12 | "include": ["./src"], 13 | "references": [{ "path": "./tsconfig.node.json" }] 14 | } 15 | -------------------------------------------------------------------------------- /packages/app/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/app/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react'; 2 | import jotaiDebugLabel from 'jotai/babel/plugin-debug-label'; 3 | import jotaiReactRefresh from 'jotai/babel/plugin-react-refresh'; 4 | import './load.envs.ts'; 5 | import { defineConfig } from 'vite'; 6 | import tsconfigPaths from 'vite-tsconfig-paths'; 7 | 8 | const WHITELIST = ['NODE_ENV', 'PUBLIC_URL']; 9 | const ENV_VARS = Object.entries(process.env).filter(([key]) => 10 | WHITELIST.some((k) => k === key || key.match(/^VITE_/)) 11 | ); 12 | 13 | const { PORT, NODE_ENV } = process.env; 14 | const PORT_CONFIG = NODE_ENV === 'test' ? 3001 : 3000; 15 | const SERVER_PORT = PORT ? Number(PORT) : PORT_CONFIG; 16 | 17 | // https://vitejs.dev/config/ 18 | export default defineConfig({ 19 | base: process.env.PUBLIC_URL || '/', 20 | build: { 21 | target: ['es2020'], 22 | outDir: process.env.BUILD_PATH || 'dist', 23 | }, 24 | plugins: [ 25 | react({ 26 | babel: { plugins: [jotaiDebugLabel, jotaiReactRefresh] }, 27 | }), 28 | tsconfigPaths(), 29 | ], 30 | server: { 31 | port: SERVER_PORT, 32 | }, 33 | define: { 34 | 'process.env': Object.fromEntries(ENV_VARS), 35 | }, 36 | ...(Boolean(process.env.CI) && { logLevel: 'silent' }), 37 | /** 38 | * Need because of this issue: 39 | * https://github.com/vitejs/vite/issues/8644#issuecomment-1159308803 40 | */ 41 | esbuild: { 42 | logOverride: { 'this-is-undefined-in-esm': 'silent' }, 43 | }, 44 | }); 45 | -------------------------------------------------------------------------------- /packages/config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@swayswap/config", 3 | "version": "0.1.0", 4 | "files": [ 5 | "./eslint.js", 6 | "./tsup.js" 7 | ], 8 | "license": "Apache-2.0", 9 | "dependencies": { 10 | "@typescript-eslint/eslint-plugin": "^5.50.0", 11 | "@typescript-eslint/parser": "^5.50.0", 12 | "eslint": "^8.33.0", 13 | "eslint-config-airbnb-base": "^15.0.0", 14 | "eslint-config-airbnb-typescript": "^17.0.0", 15 | "eslint-plugin-cypress": "^2.12.1", 16 | "eslint-config-prettier": "^8.6.0", 17 | "eslint-plugin-eslint-comments": "^3.2.0", 18 | "eslint-plugin-import": "^2.27.5", 19 | "eslint-plugin-jest-dom": "^4.0.3", 20 | "eslint-plugin-jsx-a11y": "^6.7.1", 21 | "eslint-plugin-prettier": "^4.2.1", 22 | "eslint-plugin-react": "^7.32.2", 23 | "eslint-plugin-react-hooks": "^4.6.0", 24 | "eslint-plugin-testing-library": "^5.10.0", 25 | "prettier": "^2.8.3", 26 | "react": "^18.2.0", 27 | "react-dom": "^18.2.0" 28 | }, 29 | "devDependencies": { 30 | "tsdx": "^0.14.1", 31 | "tsup": "^6.5.0", 32 | "typescript": "^4.9.5" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/config/react-imports.js: -------------------------------------------------------------------------------- 1 | // NOTE: This file should not be edited 2 | // see @configs/tsup for implementation. 3 | // - https://esbuild.github.io/content-types/#auto-import-for-jsx 4 | // - https://github.com/egoist/tsup/issues/390#issuecomment-933488738 5 | 6 | import * as React from 'react'; 7 | 8 | export { React }; 9 | -------------------------------------------------------------------------------- /packages/config/tsup.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | const path = require('path'); 4 | 5 | module.exports = (_options, { withReact } = {}) => ({ 6 | format: ['cjs', 'esm'], 7 | splitting: false, 8 | sourcemap: true, 9 | clean: false, 10 | minify: process.env.NODE_ENV === 'production', 11 | ...(withReact && { inject: [path.resolve(__dirname, './react-imports.js')] }), 12 | }); 13 | -------------------------------------------------------------------------------- /packages/contracts/README.md: -------------------------------------------------------------------------------- 1 | ## SwaySwap Contracts 2 | 3 | ### 📗 - Contracts overview 4 | 5 | - [exchange_abi](./exchange_abi) Exchange Contract interface declarations 6 | - [exchange_contract](./exchange_contract/) Exchange Contract implementation 7 | - [token_abi](./token_abi/) Token Contract interface declarations 8 | - [token_contract](./token_contract/) Token Contract implementation -------------------------------------------------------------------------------- /packages/contracts/exchange_abi/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | target 3 | -------------------------------------------------------------------------------- /packages/contracts/exchange_abi/Forc.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | authors = ["Fuel Labs "] 3 | entry = "main.sw" 4 | license = "Apache-2.0" 5 | name = "exchange_abi" 6 | -------------------------------------------------------------------------------- /packages/contracts/exchange_contract/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | target 3 | -------------------------------------------------------------------------------- /packages/contracts/exchange_contract/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Fuel Labs "] 3 | edition = "2021" 4 | license = "Apache-2.0" 5 | name = "tests" 6 | version = "0.0.0" 7 | 8 | [dependencies] 9 | fuels = "0.39.0" 10 | rand = "0.8.5" 11 | tokio = { version = "1.21.0", features = ["rt", "macros"] } 12 | 13 | [[test]] 14 | harness = true 15 | name = "tests" 16 | path = "tests/harness.rs" 17 | -------------------------------------------------------------------------------- /packages/contracts/exchange_contract/Forc.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | authors = ["Fuel Labs "] 3 | entry = "main.sw" 4 | license = "Apache-2.0" 5 | name = "exchange_contract" 6 | 7 | [dependencies] 8 | exchange_abi = { path = "../exchange_abi" } 9 | swayswap_helpers = { path = "../swayswap_helpers" } 10 | -------------------------------------------------------------------------------- /packages/contracts/swayswap_contract/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | target 3 | -------------------------------------------------------------------------------- /packages/contracts/swayswap_contract/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Fuel Labs "] 3 | edition = "2021" 4 | license = "Apache-2.0" 5 | name = "tests" 6 | version = "0.0.0" 7 | 8 | [dependencies] 9 | fuel-gql-client = { version = "0.6", default-features = false } 10 | fuel-tx = "0.9" 11 | fuels-abigen-macro = "0.13" 12 | fuels = "0.13" 13 | rand = "0.8" 14 | tokio = { version = "1.15", features = ["rt", "macros"] } 15 | 16 | [[test]] 17 | harness = true 18 | name = "tests" 19 | path = "tests/harness.rs" 20 | -------------------------------------------------------------------------------- /packages/contracts/swayswap_contract/Forc.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | authors = ["Fuel Labs "] 3 | entry = "main.sw" 4 | license = "Apache-2.0" 5 | name = "swayswap_contract" 6 | 7 | [dependencies] 8 | swayswap_helpers = { path = "../swayswap_helpers" } 9 | -------------------------------------------------------------------------------- /packages/contracts/swayswap_contract/src/main.sw: -------------------------------------------------------------------------------- 1 | contract; 2 | 3 | use std::contract_id::ContractId; 4 | use swayswap_helpers::{store_b256, get_b256}; 5 | 6 | abi SwapSwap { 7 | // Add exchange contract to the token 8 | fn add_exchange_contract(token_id: ContractId, exchange_id: ContractId); 9 | // Get exchange contract for desired token 10 | fn get_exchange_contract(token_id: ContractId) -> ContractId; 11 | } 12 | 13 | impl SwapSwap for Contract { 14 | fn add_exchange_contract(token_id: ContractId, exchange_id: ContractId) { 15 | // TODO: Assert exchange contract binary to avoid non exchange contracts to be saved 16 | store_b256(token_id.into(), exchange_id.into()); 17 | } 18 | fn get_exchange_contract(token_id: ContractId) -> ContractId { 19 | ~ContractId::from(get_b256(token_id.into())) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/contracts/swayswap_helpers/Forc.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | authors = ["Fuel Labs "] 3 | entry = "main.sw" 4 | license = "Apache-2.0" 5 | name = "swayswap_helpers" 6 | -------------------------------------------------------------------------------- /packages/contracts/swayswap_helpers/src/main.sw: -------------------------------------------------------------------------------- 1 | library swayswap_helpers; 2 | 3 | use std::{ 4 | auth::msg_sender, 5 | }; 6 | 7 | /// Return the sender as an Address or panic 8 | pub fn get_msg_sender_address_or_panic() -> Address { 9 | let sender = msg_sender(); 10 | if let Identity::Address(address) = sender.unwrap() { 11 | address 12 | } else { 13 | revert(420); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/contracts/token_abi/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | target 3 | -------------------------------------------------------------------------------- /packages/contracts/token_abi/Forc.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | authors = ["Fuel Labs "] 3 | entry = "main.sw" 4 | license = "Apache-2.0" 5 | name = "token_abi" 6 | -------------------------------------------------------------------------------- /packages/contracts/token_abi/src/main.sw: -------------------------------------------------------------------------------- 1 | library token_abi; 2 | 3 | use std::{address::Address, contract_id::ContractId, token::*}; 4 | 5 | abi Token { 6 | // Initialize contract 7 | #[storage(read, write)] 8 | fn initialize(mint_amount: u64, address: Address); 9 | // Set mint amount for each address 10 | #[storage(read, write)] 11 | fn set_mint_amount(mint_amount: u64); 12 | // Get balance of the contract coins 13 | fn get_balance() -> u64; 14 | // Return the mint amount 15 | #[storage(read)] 16 | fn get_mint_amount() -> u64; 17 | // Get balance of a specified token on contract 18 | #[payable] 19 | fn get_token_balance(asset_id: ContractId) -> u64; 20 | // Mint token coins 21 | #[storage(read)] 22 | fn mint_coins(mint_amount: u64); 23 | // Burn token coins 24 | #[storage(read)] 25 | fn burn_coins(burn_amount: u64); 26 | // Transfer a contract coins to a given output 27 | #[storage(read)] 28 | fn transfer_coins(coins: u64, address: Address); 29 | // Transfer a specified token from the contract to a given output 30 | #[storage(read)] 31 | fn transfer_token_to_output(coins: u64, asset_id: ContractId, address: Address); 32 | // Method called from address to mint coins 33 | #[storage(read, write)] 34 | fn mint(); 35 | #[storage(read)] 36 | fn has_mint(address: Address) -> bool; 37 | } 38 | -------------------------------------------------------------------------------- /packages/contracts/token_contract/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | target 3 | -------------------------------------------------------------------------------- /packages/contracts/token_contract/.rustc_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "rustc_fingerprint": 4025667977361673173, 3 | "outputs": { 4 | "931469667778813386": { 5 | "success": true, 6 | "status": "", 7 | "code": 0, 8 | "stdout": "___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n/Users/mohammad/.rustup/toolchains/stable-aarch64-apple-darwin\ndebug_assertions\nproc_macro\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"unix\"\ntarget_os=\"macos\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"apple\"\nunix\n", 9 | "stderr": "" 10 | }, 11 | "2797684049618456168": { 12 | "success": true, 13 | "status": "", 14 | "code": 0, 15 | "stdout": "___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n", 16 | "stderr": "" 17 | }, 18 | "5309432699494263626": { 19 | "success": true, 20 | "status": "", 21 | "code": 0, 22 | "stdout": "___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n", 23 | "stderr": "" 24 | }, 25 | "17598535894874457435": { 26 | "success": true, 27 | "status": "", 28 | "code": 0, 29 | "stdout": "rustc 1.59.0 (9d1b2106e 2022-02-23)\nbinary: rustc\ncommit-hash: 9d1b2106e23b1abd32fce1f17267604a5102f57a\ncommit-date: 2022-02-23\nhost: aarch64-apple-darwin\nrelease: 1.59.0\nLLVM version: 13.0.0\n", 30 | "stderr": "" 31 | } 32 | }, 33 | "successes": {} 34 | } 35 | -------------------------------------------------------------------------------- /packages/contracts/token_contract/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Fuel Labs "] 3 | edition = "2021" 4 | license = "Apache-2.0" 5 | name = "tests" 6 | version = "0.0.0" 7 | 8 | [dependencies] 9 | fuels = "0.39.0" 10 | rand = "0.8.5" 11 | tokio = { version = "1.21.0", features = ["rt", "macros"] } 12 | 13 | [[test]] 14 | harness = true 15 | name = "tests" 16 | path = "tests/harness.rs" 17 | -------------------------------------------------------------------------------- /packages/contracts/token_contract/Forc.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | authors = ["Fuel Labs "] 3 | entry = "main.sw" 4 | license = "Apache-2.0" 5 | name = "token_contract" 6 | 7 | [dependencies] 8 | token_abi = { path = "../token_abi" } 9 | swayswap_helpers = { path = "../swayswap_helpers" } 10 | -------------------------------------------------------------------------------- /packages/scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swayswap-scripts", 3 | "version": "0.1.0", 4 | "license": "Apache-2.0", 5 | "files": [ 6 | "dist/" 7 | ], 8 | "main": "./dist/index.js", 9 | "bin": { 10 | "swayswap-scripts": "./dist/bin/index.js" 11 | }, 12 | "scripts": { 13 | "build": "tsup" 14 | }, 15 | "dependencies": { 16 | "bundle-require": "^3.1.2", 17 | "commander": "^10.0.0", 18 | "debug": "^4.3.4", 19 | "esbuild": "^0.16.0", 20 | "fuels": "0.35.0", 21 | "@fuel-ts/wallet": "0.7.1", 22 | "@fuel-ts/wallet-manager": "0.35.0", 23 | "joycon": "^3.1.1", 24 | "prettier": "^2.8.3", 25 | "typescript": "^4.9.5" 26 | }, 27 | "devDependencies": { 28 | "@swayswap/config": "workspace:0.1.0", 29 | "@types/debug": "^4.1.7", 30 | "@types/glob": "^8.0.1", 31 | "glob": "^8.1.0", 32 | "tsup": "^6.5.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/scripts/src/actions/buildContract.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process'; 2 | import { log } from 'src/log'; 3 | 4 | // Build contracts using forc 5 | // We assume forc is install on the local 6 | // if is not install it would result on 7 | // throwing a error 8 | export async function buildContract(path: string) { 9 | log('Build', path); 10 | return new Promise((resolve, reject) => { 11 | const forcBuild = spawn('forc', ['build', '-p', path], { stdio: 'inherit' }); 12 | forcBuild.on('exit', (code) => { 13 | if (!code) return resolve(code); 14 | forcBuild.kill(); 15 | reject(); 16 | }); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /packages/scripts/src/actions/buildContracts.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-syntax */ 2 | 3 | import type { Config } from 'src/types'; 4 | 5 | import { buildContract } from './buildContract'; 6 | import { buildTypes } from './buildTypes'; 7 | import { prettifyContracts } from './prettifyContracts'; 8 | 9 | export async function buildContracts(config: Config) { 10 | for (const { path } of config.contracts) { 11 | await buildContract(path); 12 | } 13 | await buildTypes(config); 14 | await prettifyContracts(config); 15 | } 16 | -------------------------------------------------------------------------------- /packages/scripts/src/actions/buildTypes.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process'; 2 | import type { Config } from 'src/types'; 3 | // Generate types 4 | export async function buildTypes(config: Config) { 5 | return new Promise((resolve, reject) => { 6 | const typeGeneration = spawn('pnpm', [ 7 | 'exec', 8 | 'fuels', 9 | 'typegen', 10 | '-i', 11 | config.types.artifacts, 12 | '-o', 13 | config.types.output, 14 | ]); 15 | typeGeneration.on('exit', (code) => { 16 | if (!code) return resolve(code); 17 | typeGeneration.kill(); 18 | reject(); 19 | }); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /packages/scripts/src/actions/deployContractBinary.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import type { JsonAbi, WalletUnlocked } from 'fuels'; 3 | import { ContractFactory } from 'fuels'; 4 | import path from 'path'; 5 | import { log } from 'src/log'; 6 | import type { DeployContractOptions } from 'src/types'; 7 | 8 | function getBinaryName(contractPath: string) { 9 | const fileName = contractPath.split('/').slice(-1); 10 | return `/out/debug/${fileName}.bin`; 11 | } 12 | 13 | function getABIName(contractPath: string) { 14 | const fileName = contractPath.split('/').slice(-1); 15 | return `/out/debug/${fileName}-abi.json`; 16 | } 17 | 18 | export async function deployContractBinary( 19 | wallet: WalletUnlocked, 20 | binaryPath: string, 21 | options?: DeployContractOptions 22 | ) { 23 | if (!wallet) { 24 | throw new Error('Cannot deploy without wallet'); 25 | } 26 | const binaryFilePath = path.join(binaryPath, getBinaryName(binaryPath)); 27 | const abiFilePath = path.join(binaryPath, getABIName(binaryPath)); 28 | log('read binary file from: ', binaryFilePath); 29 | const bytecode = readFileSync(binaryFilePath); 30 | const abiJSON = JSON.parse(readFileSync(abiFilePath).toString()) as JsonAbi; 31 | const contractFactory = new ContractFactory(bytecode, abiJSON, wallet); 32 | 33 | log('deploy contract'); 34 | const contract = await contractFactory.deployContract({ 35 | gasLimit: 1_000_000, 36 | storageSlots: [], 37 | ...options, 38 | }); 39 | log('contract successful deployed'); 40 | return contract.id.toB256(); 41 | } 42 | -------------------------------------------------------------------------------- /packages/scripts/src/actions/deployContracts.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-syntax */ 2 | import type { Config, ContractDeployed, DeployContractOptions } from 'src/types'; 3 | 4 | import { deployContractBinary } from './deployContractBinary'; 5 | import { getWalletInstance } from './getWalletInstance'; 6 | 7 | export async function deployContracts(config: Config) { 8 | const wallet = await getWalletInstance(); 9 | const contracts: Array = []; 10 | 11 | for (const { name, path, options } of config.contracts) { 12 | let contractOptions: DeployContractOptions | undefined; 13 | 14 | if (typeof options === 'function') { 15 | contractOptions = options(contracts); 16 | } else if (typeof options === 'object') { 17 | contractOptions = options; 18 | } 19 | 20 | contracts.push({ 21 | name, 22 | contractId: await deployContractBinary(wallet, path, contractOptions), 23 | }); 24 | } 25 | 26 | return contracts; 27 | } 28 | -------------------------------------------------------------------------------- /packages/scripts/src/actions/getWalletInstance.ts: -------------------------------------------------------------------------------- 1 | import { WalletManager } from '@fuel-ts/wallet-manager'; 2 | import { Wallet } from 'fuels'; 3 | import { log } from 'src/log'; 4 | 5 | export async function getWalletInstance() { 6 | // Avoid early load of process env 7 | const { WALLET_SECRET, GENESIS_SECRET, PROVIDER_URL } = process.env; 8 | 9 | if (WALLET_SECRET) { 10 | log('WALLET_SECRET detected'); 11 | if (WALLET_SECRET && WALLET_SECRET.indexOf(' ') >= 0) { 12 | const walletManager = new WalletManager(); 13 | const password = '0b540281-f87b-49ca-be37-2264c7f260f7'; 14 | 15 | await walletManager.unlock(password); 16 | const config = { type: 'mnemonic', secret: WALLET_SECRET }; 17 | // Add a vault of type mnemonic 18 | await walletManager.addVault(config); 19 | await walletManager.addAccount(); 20 | const accounts = walletManager.getAccounts(); 21 | 22 | const wallet = walletManager.getWallet(accounts[0].address); 23 | wallet.connect(PROVIDER_URL!); 24 | return wallet; 25 | } 26 | 27 | return Wallet.fromPrivateKey(WALLET_SECRET!, PROVIDER_URL); 28 | } 29 | // If no WALLET_SECRET is informed we assume 30 | // We are on a test environment 31 | // In this case it must provide a GENESIS_SECRET 32 | // on this case the origen of balances should be 33 | // almost limitless assuming the genesis has enough 34 | // balances configured 35 | if (GENESIS_SECRET) { 36 | log('Funding wallet with some coins'); 37 | return Wallet.generate(); 38 | } 39 | throw new Error('You must provide a WALLET_SECRET or GENESIS_SECRET'); 40 | } 41 | -------------------------------------------------------------------------------- /packages/scripts/src/actions/prettifyContracts.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process'; 2 | import type { Config } from 'src/types'; 3 | 4 | export async function prettifyContracts(config: Config) { 5 | return new Promise((resolve, reject) => { 6 | const prettifyProcess = spawn( 7 | 'node_modules/.bin/prettier', 8 | ['--write', config.types.output.replace(/ˆ\.\//, '')], 9 | { 10 | stdio: 'inherit', 11 | } 12 | ); 13 | prettifyProcess.on('exit', (code) => { 14 | if (!code) return resolve(code); 15 | prettifyProcess.kill(); 16 | reject(); 17 | }); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /packages/scripts/src/actions/runAll.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '../types'; 2 | 3 | import { buildContracts } from './buildContracts'; 4 | import { deployContracts } from './deployContracts'; 5 | 6 | export async function runAll(config: Config) { 7 | await buildContracts(config); 8 | const contractIds = await deployContracts(config); 9 | return contractIds; 10 | } 11 | -------------------------------------------------------------------------------- /packages/scripts/src/actions/validateConfig.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'src/types'; 2 | 3 | export function validateConfig(config: Config) { 4 | if (!Array.isArray(config.contracts)) { 5 | throw new Error('config.contract should be a valid array'); 6 | } 7 | if (typeof config.types.artifacts !== 'string') { 8 | throw new Error('config.types.artifacts should be a valid string'); 9 | } 10 | if (typeof config.types.output !== 'string') { 11 | throw new Error('config.types.output should be a valid string'); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/scripts/src/bin/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('./bin.js'); 4 | -------------------------------------------------------------------------------- /packages/scripts/src/helpers/createConfig.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '../types'; 2 | 3 | export function createConfig(config: Config) { 4 | return config; 5 | } 6 | -------------------------------------------------------------------------------- /packages/scripts/src/helpers/loader.ts: -------------------------------------------------------------------------------- 1 | import { bundleRequire } from 'bundle-require'; 2 | import JoyCon from 'joycon'; 3 | import path from 'path'; 4 | import { validateConfig } from 'src/actions/validateConfig'; 5 | import type { Config } from 'src/types'; 6 | 7 | export async function loadConfig(cwd: string): Promise { 8 | const configJoycon = new JoyCon(); 9 | const configPath = await configJoycon.resolve({ 10 | files: ['swayswap.config.js', 'swayswap.config.ts'], 11 | cwd, 12 | stopDir: path.parse(cwd).root, 13 | packageKey: 'tsup', 14 | }); 15 | 16 | if (configPath) { 17 | const result = await bundleRequire({ 18 | filepath: configPath, 19 | }); 20 | const config = result.mod.default; 21 | 22 | if (config.env) { 23 | // If env config is provide override current 24 | // process.env with new envs 25 | Object.assign(process.env, config.env); 26 | } 27 | 28 | validateConfig(config); 29 | 30 | return config; 31 | } 32 | 33 | return { 34 | types: { 35 | artifacts: '', 36 | output: '', 37 | }, 38 | contracts: [], 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /packages/scripts/src/helpers/replaceEventOnEnv.ts: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from 'fs/promises'; 2 | import { log } from 'src/log'; 3 | import type { Event } from 'src/types'; 4 | import { Commands } from 'src/types'; 5 | 6 | // TODO: This file should be placed inside the 7 | // swayswap.config.ts but for now as the app 8 | // uses es5 and esbuild didn't support we have 9 | // add it here as a helper function 10 | 11 | /** 12 | * Use event output data to replace 13 | * on the provide path env the new 14 | * contract ids. 15 | * 16 | * It uses the name inform on the config.contracts.name 17 | * as a key to the new value. If it didn't found the key 18 | * on the provide path nothing happens 19 | */ 20 | export async function replaceEventOnEnv(path: string, event: Event) { 21 | if (event.type === Commands.deploy || event.type === Commands.run) { 22 | log(`Reading file from ${path}`); 23 | const fileEnv = (await readFile(path)).toString(); 24 | // Replace new ides on .env file 25 | const newEnvFile = event.data.reduce((file, { name, contractId }) => { 26 | log(`Replace env ${name} with ${contractId}`); 27 | // Replace key with new value 28 | return file.replace(new RegExp(`(${name}=).*`), `$1${contractId}`); 29 | }, fileEnv); 30 | log(`Updating ${path} with new contract ids`); 31 | await writeFile(path, newEnvFile); 32 | log(`${path} contract updates!`); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/scripts/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './helpers/createConfig'; 2 | export * from './helpers/replaceEventOnEnv'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /packages/scripts/src/log.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | export function log(...data: any[]) { 4 | console.log(...data); 5 | } 6 | -------------------------------------------------------------------------------- /packages/scripts/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { BytesLike, CreateTransactionRequestLike, StorageSlot } from 'fuels'; 2 | 3 | export type DeployContractOptions = { 4 | salt?: BytesLike; 5 | storageSlots?: StorageSlot[]; 6 | stateRoot?: BytesLike; 7 | } & CreateTransactionRequestLike; 8 | 9 | export enum Commands { 10 | 'build' = 'build', 11 | 'deploy' = 'deploy', 12 | 'types' = 'types', 13 | 'run' = 'run', 14 | } 15 | 16 | export type BuildDeploy = { 17 | name: string; 18 | contractId: string; 19 | }; 20 | 21 | export type Event = 22 | | { 23 | type: Commands.build; 24 | data: unknown; 25 | } 26 | | { 27 | type: Commands.deploy; 28 | data: Array; 29 | } 30 | | { 31 | type: Commands.run; 32 | data: Array; 33 | }; 34 | 35 | export type OptionsFunction = (contracts: Array) => DeployContractOptions; 36 | 37 | export type ContractConfig = { 38 | name: string; 39 | path: string; 40 | options?: DeployContractOptions | OptionsFunction; 41 | }; 42 | 43 | export type ContractDeployed = { 44 | name: string; 45 | contractId: string; 46 | }; 47 | 48 | export type Config = { 49 | onSuccess?: (event: Event) => void; 50 | onFailure?: (err: unknown) => void; 51 | env?: { 52 | [key: string]: string; 53 | }; 54 | types: { 55 | artifacts: string; 56 | output: string; 57 | }; 58 | contracts: Array; 59 | }; 60 | -------------------------------------------------------------------------------- /packages/scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "baseUrl": ".", 6 | "rootDir": "." 7 | }, 8 | "include": ["src", "./setup.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/scripts/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig((options) => [ 4 | { 5 | entry: ['src/bin/index.ts', 'src/index.ts'], 6 | clean: true, 7 | dts: { 8 | entry: './src/index.ts', 9 | }, 10 | format: ['cjs', 'esm'], 11 | minify: !options.watch, 12 | }, 13 | ]); 14 | -------------------------------------------------------------------------------- /packages/test-utils/config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@jest/types'; 2 | 3 | export const config: Config.InitialOptions = { 4 | preset: 'ts-jest/presets/default-esm', 5 | globals: { 6 | 'ts-jest': { 7 | useESM: true, 8 | }, 9 | }, 10 | testTimeout: 20000, 11 | testEnvironment: 'jsdom', 12 | testMatch: ['/**/?(*.)+(spec|test).[jt]s?(x)'], 13 | testPathIgnorePatterns: ['/node_modules/', '/dist/', '/cypress'], 14 | modulePathIgnorePatterns: ['/dist/'], 15 | reporters: ['default', 'github-actions'], 16 | setupFiles: ['dotenv/config'], 17 | setupFilesAfterEnv: ['@swayswap/test-utils/setup.ts'], 18 | collectCoverageFrom: [ 19 | '/src/**/*.{ts,tsx}', 20 | '!/src/**/*d.ts', 21 | '!/src/**/*test.{ts,tsx}', 22 | '!/src/**/test-*.{ts}', 23 | '!/src/**/__mocks__/**', 24 | '!/src/types/**', 25 | ], 26 | moduleNameMapper: { 27 | '.+\\.(css|scss|png|jpg|svg)$': 'jest-transform-stub', 28 | '~/(.*)$': '/src/$1', 29 | '^(\\.{1,2}/.*)\\.js$': '$1', 30 | }, 31 | }; 32 | 33 | export default config; 34 | -------------------------------------------------------------------------------- /packages/test-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@swayswap/test-utils", 3 | "version": "0.1.0", 4 | "license": "Apache-2.0", 5 | "main": "src/index.ts", 6 | "publishConfig": { 7 | "main": "dist/index.js", 8 | "module": "dist/index.mjs", 9 | "types": "dist/index.d.ts", 10 | "typings": "dist/index.d.ts", 11 | "exports": { 12 | ".": { 13 | "require": "./dist/index.js", 14 | "default": "./dist/index.mjs" 15 | } 16 | }, 17 | "files": [ 18 | "dist", 19 | "config.ts", 20 | "setup.ts" 21 | ] 22 | }, 23 | "scripts": { 24 | "build": "tsup --dts" 25 | }, 26 | "dependencies": { 27 | "@chakra-ui/utils": "2.0.14", 28 | "@testing-library/dom": "^8.20.0", 29 | "@testing-library/jest-dom": "^5.16.5", 30 | "@testing-library/react": "^13.4.0", 31 | "@testing-library/react-hooks": "^8.0.1", 32 | "@testing-library/user-event": "^14.4.3", 33 | "@types/testing-library__jest-dom": "^5.14.5", 34 | "jest-axe": "^7.0.0", 35 | "jest-fail-on-console": "^3.0.2", 36 | "jest-matcher-utils": "^29.4.1", 37 | "react": "^18.2.0", 38 | "react-dom": "^18.2.0", 39 | "react-router-dom": "^6.8.0" 40 | }, 41 | "devDependencies": { 42 | "@swayswap/config": "workspace:*", 43 | "@types/jest": "^28.1.8", 44 | "@types/jest-axe": "^3.5.5" 45 | }, 46 | "peerDependencies": { 47 | "react": ">=18" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/test-utils/setup.ts: -------------------------------------------------------------------------------- 1 | import failOnConsole from 'jest-fail-on-console'; 2 | 3 | const { getComputedStyle } = window; 4 | window.getComputedStyle = (elt) => getComputedStyle(elt); 5 | 6 | if (typeof window.matchMedia !== 'function') { 7 | Object.defineProperty(window, 'matchMedia', { 8 | enumerable: true, 9 | configurable: true, 10 | writable: true, 11 | value: jest.fn().mockImplementation((query) => ({ 12 | matches: false, 13 | media: query, 14 | onchange: null, 15 | addListener: jest.fn(), // Deprecated 16 | removeListener: jest.fn(), // Deprecated 17 | addEventListener: jest.fn(), 18 | removeEventListener: jest.fn(), 19 | dispatchEvent: jest.fn(), 20 | })), 21 | }); 22 | } 23 | 24 | failOnConsole({ 25 | silenceMessage: (msg, method) => { 26 | if (msg.includes('toHexString')) return true; 27 | if (msg.includes('ReactDOM.render is no longer supported in React 18')) return true; 28 | if (method === 'warn') return true; 29 | return false; 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /packages/test-utils/src/accessibility.ts: -------------------------------------------------------------------------------- 1 | import type { RenderOptions } from '@testing-library/react'; 2 | import type { JestAxeConfigureOptions } from 'jest-axe'; 3 | import { axe, toHaveNoViolations } from 'jest-axe'; 4 | import { isValidElement } from 'react'; 5 | 6 | import { render } from './render'; 7 | 8 | export async function testA11y( 9 | ui: React.ReactElement | HTMLElement, 10 | options: RenderOptions & { axeOptions?: JestAxeConfigureOptions } = {} 11 | ) { 12 | const { axeOptions, ...rest } = options; 13 | const container = isValidElement(ui) ? render(ui, rest).container : ui; 14 | const results = await axe(container, axeOptions); 15 | expect(results).toHaveNoViolations(); 16 | } 17 | 18 | expect.extend(toHaveNoViolations); 19 | -------------------------------------------------------------------------------- /packages/test-utils/src/focus.ts: -------------------------------------------------------------------------------- 1 | import { getActiveElement, isFocusable } from '@chakra-ui/utils'; 2 | import { act } from '@testing-library/react'; 3 | 4 | export function focus(el: HTMLElement) { 5 | if (getActiveElement(el) === el) return; 6 | if (!isFocusable(el)) return; 7 | act(() => { 8 | el.focus(); 9 | }); 10 | } 11 | 12 | export function blur(el?: HTMLElement | null) { 13 | // eslint-disable-next-line no-param-reassign 14 | if (el == null) el = document.activeElement as HTMLElement; 15 | if (el.tagName === 'BODY') return; 16 | if (getActiveElement(el) !== el) return; 17 | act(() => { 18 | if (el && 'blur' in el) el.blur(); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /packages/test-utils/src/hooks.ts: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '@testing-library/react-hooks'; 2 | 3 | export const hooks = { 4 | render: renderHook, 5 | act, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/test-utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | act, 3 | fireEvent, 4 | screen, 5 | waitFor, 6 | RenderResult, 7 | waitForElementToBeRemoved, 8 | queryByText, 9 | } from '@testing-library/react'; 10 | export * from './accessibility'; 11 | export { blur, focus } from './focus'; 12 | export * from './hooks'; 13 | export * from './mocks'; 14 | export * from './press'; 15 | export * from './render'; 16 | -------------------------------------------------------------------------------- /packages/test-utils/src/mocks/cookie.ts: -------------------------------------------------------------------------------- 1 | export function mockCookieStorage(key: string, value: string | null) { 2 | Object.defineProperty(document, 'cookie', { 3 | writable: true, 4 | value: value ? `${key}=${value}` : '', 5 | }); 6 | } 7 | -------------------------------------------------------------------------------- /packages/test-utils/src/mocks/image.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-constructor-return */ 2 | type Status = 'loaded' | 'error'; 3 | 4 | const originalImage = window.Image; 5 | 6 | export function mockImage() { 7 | let status: Status; 8 | 9 | // @ts-expect-error 10 | window.Image = class Image { 11 | onload: VoidFunction = () => { 12 | // eslint-disable-next-line no-console 13 | console.log('called'); 14 | }; 15 | 16 | onerror: VoidFunction = () => {}; 17 | src = ''; 18 | alt = ''; 19 | hasAttribute(name: string) { 20 | return name in this; 21 | } 22 | 23 | getAttribute(name: string) { 24 | return name in this ? (this as any)[name] : null; // eslint-disable-line @typescript-eslint/no-explicit-any 25 | } 26 | 27 | constructor() { 28 | setTimeout(() => { 29 | if (status === 'error') { 30 | this.onerror(); 31 | } else { 32 | this.onload(); 33 | } 34 | }, mockImage.DELAY); 35 | return this; 36 | } 37 | }; 38 | 39 | return { 40 | simulate(value: Status) { 41 | status = value; 42 | }, 43 | restore() { 44 | window.Image = originalImage; 45 | }, 46 | }; 47 | } 48 | 49 | mockImage.restore = () => { 50 | window.Image = originalImage; 51 | }; 52 | 53 | mockImage.DELAY = 100; 54 | -------------------------------------------------------------------------------- /packages/test-utils/src/mocks/index.ts: -------------------------------------------------------------------------------- 1 | import { mockCookieStorage } from './cookie'; 2 | import { mockImage } from './image'; 3 | import { mockLocalStorage } from './localstorage'; 4 | import { mockMatchMedia } from './match-media'; 5 | 6 | export const mocks = { 7 | image: mockImage, 8 | cookie: mockCookieStorage, 9 | localStorage: mockLocalStorage, 10 | matchMedia: mockMatchMedia, 11 | }; 12 | -------------------------------------------------------------------------------- /packages/test-utils/src/mocks/localstorage.ts: -------------------------------------------------------------------------------- 1 | export function mockLocalStorage(initialStore: { [key: string]: string }) { 2 | let store = initialStore; 3 | 4 | const newLocalStorage = { 5 | writable: true, 6 | value: { 7 | getItem(key: string) { 8 | return store[key] || null; 9 | }, 10 | setItem(key: string, value: string) { 11 | store[key] = value.toString(); 12 | }, 13 | removeItem(key: string) { 14 | delete store[key]; 15 | }, 16 | clear() { 17 | store = {}; 18 | }, 19 | }, 20 | }; 21 | 22 | Object.defineProperty(window, 'localStorage', newLocalStorage); 23 | } 24 | -------------------------------------------------------------------------------- /packages/test-utils/src/mocks/match-media.ts: -------------------------------------------------------------------------------- 1 | export function mockMatchMedia(media: string, matches: boolean) { 2 | const desc: PropertyDescriptor = { 3 | writable: true, 4 | configurable: true, 5 | enumerable: true, 6 | value: () => ({ 7 | matches, 8 | media, 9 | addEventListener: jest.fn(), 10 | addListener: jest.fn(), 11 | removeEventListener: jest.fn(), 12 | removeListener: jest.fn(), 13 | dispatchEvent: jest.fn(), 14 | }), 15 | }; 16 | 17 | Object.defineProperty(window, 'matchMedia', desc); 18 | } 19 | -------------------------------------------------------------------------------- /packages/test-utils/src/render.tsx: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom"; 2 | import type { RenderOptions } from "@testing-library/react"; 3 | import { render as rtlRender } from "@testing-library/react"; 4 | import type { ReactNode } from "react"; 5 | import { MemoryRouter } from "react-router-dom"; 6 | 7 | import { userEvent } from "./user-event"; 8 | 9 | export function render( 10 | ui: React.ReactElement, 11 | options: RenderOptions = {} 12 | ): ReturnType & { user: ReturnType } { 13 | const user = userEvent.setup(); 14 | const result = rtlRender(ui, options); 15 | return { user, ...result }; 16 | } 17 | 18 | function wrapper(route?: string) { 19 | return ({ children }: { children: ReactNode }) => ( 20 | 21 | {children} 22 | 23 | ); 24 | } 25 | 26 | export function renderWithRouter( 27 | ui: React.ReactElement, 28 | { route, ...options }: RenderOptions & { route?: string } = { route: "/" } 29 | ): ReturnType & { user: ReturnType } { 30 | const user = userEvent.setup(); 31 | const result = rtlRender(ui, { ...options, wrapper: wrapper(route) }); 32 | return { user, ...result }; 33 | } 34 | -------------------------------------------------------------------------------- /packages/test-utils/src/user-event.ts: -------------------------------------------------------------------------------- 1 | import { act } from '@testing-library/react'; 2 | import $userEvent from '@testing-library/user-event'; 3 | 4 | import { press } from './press'; 5 | import { sleep } from './utils'; 6 | 7 | type Writeable = { -readonly [P in keyof T]: T[P] }; 8 | 9 | type PatchResult = Omit, 'setup'> & { 10 | press: typeof press; 11 | setup: (...args: any[]) => PatchResult; // eslint-disable-line @typescript-eslint/no-explicit-any 12 | }; 13 | 14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 15 | function patch($value: any) { 16 | const result = Object.entries($value).reduce((acc, [key, value]) => { 17 | if (key === 'setup') { 18 | // @ts-expect-error 19 | acc[key] = (...args: any[]) => ({ ...patch(value(...args)), press }); // eslint-disable-line @typescript-eslint/no-explicit-any 20 | } else { 21 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 22 | acc[key] = async (...args: any[]) => { 23 | act(() => { 24 | // @ts-expect-error 25 | value(...args); 26 | }); 27 | await sleep(); 28 | }; 29 | } 30 | 31 | return acc; 32 | }, {} as any); // eslint-disable-line @typescript-eslint/no-explicit-any 33 | 34 | return result as PatchResult; 35 | } 36 | 37 | const userEvent = { ...patch($userEvent), press }; 38 | 39 | export { userEvent }; 40 | -------------------------------------------------------------------------------- /packages/test-utils/src/utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-promise-executor-return */ 2 | import { act } from '@testing-library/react'; 3 | 4 | export function queue(): Promise { 5 | return act(() => Promise.resolve()); 6 | } 7 | 8 | export function nextTick(): Promise { 9 | return act(() => new Promise((resolve) => requestAnimationFrame(() => resolve()))); 10 | } 11 | 12 | export async function sleep(ms = 16): Promise { 13 | await act(() => new Promise((resolve) => setTimeout(resolve, ms))); 14 | await nextTick(); 15 | } 16 | -------------------------------------------------------------------------------- /packages/test-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "baseUrl": ".", 6 | "rootDir": ".", 7 | "types": ["jest"] 8 | }, 9 | "include": ["src", "./setup.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/test-utils/tsup.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from '@swayswap/config/tsup'; 2 | import { defineConfig } from 'tsup'; 3 | 4 | export default defineConfig((options) => ({ 5 | ...baseConfig(options), 6 | external: ['react'], 7 | entry: ['src/index.ts'], 8 | treeshake: true, 9 | })); 10 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | -------------------------------------------------------------------------------- /scripts/ci-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Load env_template 4 | APP_TEST_ENV=./packages/app/.env.test 5 | TEST_CREATED=0 6 | 7 | # If packages/app/.env.test not exists 8 | # create it 9 | if [ ! -f "$APP_TEST_ENV" ]; then 10 | echo "Create $APP_TEST_ENV"; 11 | ./scripts/create-test-env.sh 4001 4041 $APP_TEST_ENV; 12 | TEST_CREATED=1; 13 | fi 14 | 15 | # Run setup 16 | export NODE_ENV=test 17 | pnpm services:setup-test-init 18 | 19 | echo $1 20 | 21 | # Run test 22 | if [ "$1" = "--coverage" ]; then 23 | pnpm test:coverage 24 | TEST_RESULT=$? 25 | elif [ "$1" = "--e2e" ]; then 26 | pnpm test:e2e 27 | TEST_RESULT=$? 28 | else 29 | pnpm test 30 | TEST_RESULT=$? 31 | fi 32 | 33 | # Run cleanup 34 | pnpm services:clean-test 35 | 36 | # After run the tests delete .env.test 37 | # If it was created 38 | if [ $TEST_CREATED == 1 ]; then 39 | rm $APP_TEST_ENV 40 | fi 41 | 42 | exit $TEST_RESULT -------------------------------------------------------------------------------- /scripts/create-test-env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ $# == 0 ]; then 4 | echo "Expect 3 args received $#" 5 | exit 1; 6 | fi; 7 | 8 | # Env file 9 | APP_TEST_ENV=$3 10 | 11 | echo "VITE_FUEL_PROVIDER_URL=http://localhost:$1/graphql" > $APP_TEST_ENV 12 | echo "VITE_FUEL_FAUCET_URL=http://localhost:$2/dispense" >> $APP_TEST_ENV 13 | echo "VITE_CONTRACT_ID=0x0000000000000000000000000000000000000000000000000000000000000000" >> $APP_TEST_ENV 14 | echo "VITE_TOKEN_ID1=0x0000000000000000000000000000000000000000000000000000000000000000" >> $APP_TEST_ENV 15 | echo "VITE_TOKEN_ID2=0x0000000000000000000000000000000000000000000000000000000000000000" >> $APP_TEST_ENV 16 | -------------------------------------------------------------------------------- /scripts/test-contracts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CURRENT_PATH=$(pwd) 4 | cd $CURRENT_PATH/packages/contracts/token_contract && cargo test 5 | cd $CURRENT_PATH/packages/contracts/exchange_contract && cargo test -------------------------------------------------------------------------------- /scripts/update-deps.sh: -------------------------------------------------------------------------------- 1 | BLACKLIST=swayswap-scripts,@swayswap/test-utils,@swayswap/config,fuels,typechain-target-fuels 2 | pnpm -r exec updates -e $BLACKLIST -u 3 | pnpm install 4 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "downlevelIteration": true, 5 | "preserveSymlinks": false, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "jsx": "react-jsx", 9 | "lib": ["dom", "esnext"], 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitAny": true, 14 | "resolveJsonModule": true, 15 | "skipLibCheck": true, 16 | "sourceMap": true, 17 | "strict": true, 18 | "strictFunctionTypes": true, 19 | "strictNullChecks": true, 20 | "strictPropertyInitialization": true, 21 | "suppressImplicitAnyIndexErrors": true, 22 | "target": "ES6" 23 | }, 24 | "exclude": ["**/dist", "**/build", "node_modules", "**/node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./tsconfig.base.json", 4 | "compilerOptions": { 5 | "noEmit": true, 6 | "allowJs": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json" 3 | } 4 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "pipeline": { 4 | "build": { 5 | "dependsOn": ["^build"], 6 | "outputs": ["dist/**", "storybook-static/**"] 7 | }, 8 | "test": { 9 | "dependsOn": [] 10 | }, 11 | "dev": { 12 | "cache": false 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "silent": true 4 | } 5 | } 6 | --------------------------------------------------------------------------------