From 2a51c3857f3e2a5b0720458c76f0cedbe04de6fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luiz=20Est=C3=A1cio=20=7C=20stacio=2Eeth?= Date: Sat, 8 Apr 2023 22:20:32 -0300 Subject: [PATCH] feat: remove welcome flow from localStorage (#406) --- .../systems/Core/components/PrivateRoute.tsx | 6 +- .../systems/Core/components/WalletWidget.tsx | 1 + .../Core/hooks/__mocks__/MockConnection.ts | 27 +- .../systems/Core/hooks/__mocks__/useWallet.ts | 33 +- .../systems/Core/hooks/useWalletConnection.ts | 28 ++ packages/app/src/systems/Mint/routes.tsx | 2 +- .../systems/Pool/pages/AddLiquidity.test.tsx | 11 +- .../app/src/systems/Pool/pages/Pools.test.tsx | 21 +- .../Pool/pages/RemoveLiquidity.test.tsx | 11 +- .../src/systems/Swap/pages/SwapPage.test.tsx | 42 +- .../systems/Welcome/components/AddAssets.tsx | 8 +- .../systems/Welcome/components/AddFunds.tsx | 2 +- .../systems/Welcome/components/MintAssets.tsx | 8 +- .../Welcome/components/WelcomeConnect.tsx | 34 +- .../Welcome/components/WelcomeStep.tsx | 13 +- .../{WelcomeDone.tsx => WelcomeTerms.tsx} | 19 +- .../src/systems/Welcome/components/index.tsx | 2 +- .../systems/Welcome/hooks/useWelcomeSteps.tsx | 411 ++-------------- .../app/src/systems/Welcome/machines/index.ts | 1 + .../Welcome/machines/welcomeMachine.test.ts | 200 ++++++++ .../Welcome/machines/welcomeMachine.ts | 462 ++++++++++++++++++ .../src/systems/Welcome/pages/WelcomePage.tsx | 28 +- packages/app/src/systems/Welcome/routes.tsx | 6 +- .../factories/ExchangeContractAbi__factory.ts | 16 +- packages/app/src/types/index.ts | 11 +- 25 files changed, 876 insertions(+), 527 deletions(-) create mode 100644 packages/app/src/systems/Core/hooks/useWalletConnection.ts rename packages/app/src/systems/Welcome/components/{WelcomeDone.tsx => WelcomeTerms.tsx} (79%) create mode 100644 packages/app/src/systems/Welcome/machines/index.ts create mode 100644 packages/app/src/systems/Welcome/machines/welcomeMachine.test.ts create mode 100644 packages/app/src/systems/Welcome/machines/welcomeMachine.ts diff --git a/packages/app/src/systems/Core/components/PrivateRoute.tsx b/packages/app/src/systems/Core/components/PrivateRoute.tsx index 2015d337..7f0e45cd 100644 --- a/packages/app/src/systems/Core/components/PrivateRoute.tsx +++ b/packages/app/src/systems/Core/components/PrivateRoute.tsx @@ -5,15 +5,13 @@ import { Navigate, useNavigate } from "react-router-dom"; import { useFuel } from "../hooks/useFuel"; -import { getCurrent, getAgreement } from "~/systems/Welcome"; +import { getAgreement } from "~/systems/Welcome"; import { Pages } from "~/types"; export function PrivateRoute({ children }: { children: ReactNode }) { - const current = getCurrent(); const navigate = useNavigate(); const acceptAgreement = getAgreement(); const { fuel, error } = useFuel(); - const { data: isConnected, isLoading } = useQuery( ["isConnected", fuel !== undefined], async () => { @@ -48,7 +46,7 @@ export function PrivateRoute({ children }: { children: ReactNode }) { }; }, [isConnected, fuel]); - if ((current.id > 4 && acceptAgreement) || (isConnected && !current.id)) { + if (acceptAgreement) { return <>{children}; } diff --git a/packages/app/src/systems/Core/components/WalletWidget.tsx b/packages/app/src/systems/Core/components/WalletWidget.tsx index a4769e69..1df713c9 100644 --- a/packages/app/src/systems/Core/components/WalletWidget.tsx +++ b/packages/app/src/systems/Core/components/WalletWidget.tsx @@ -19,6 +19,7 @@ export function WalletWidget() { function handleDisconnect() { window.localStorage.clear(); + window.fuel?.disconnect(); window.location.reload(); } diff --git a/packages/app/src/systems/Core/hooks/__mocks__/MockConnection.ts b/packages/app/src/systems/Core/hooks/__mocks__/MockConnection.ts index 3ecf1a62..ed98d8cc 100644 --- a/packages/app/src/systems/Core/hooks/__mocks__/MockConnection.ts +++ b/packages/app/src/systems/Core/hooks/__mocks__/MockConnection.ts @@ -2,9 +2,9 @@ import type { FuelWalletConnection } from '@fuel-wallet/sdk'; import { FuelWalletLocked, FuelWalletProvider, BaseConnection } from '@fuel-wallet/sdk'; -import type { FuelProviderConfig } from '@fuel-wallet/types'; +import type { Asset, FuelProviderConfig } from '@fuel-wallet/types'; import EventEmitter from 'events'; -import type { AbstractAddress, TransactionRequest } from 'fuels'; +import type { AbstractAddress, TransactionRequest, WalletUnlocked } from 'fuels'; import { Wallet, Address } from 'fuels'; import { FUEL_PROVIDER_URL } from '~/config'; @@ -12,12 +12,14 @@ import { FUEL_PROVIDER_URL } from '~/config'; const generateOptions = { provider: FUEL_PROVIDER_URL, }; -export const userWallet = Wallet.generate(generateOptions); export const toWallet = Wallet.generate(generateOptions); const events = new EventEmitter(); export class MockConnection extends BaseConnection { isConnectedOverride; + assetsData: Asset[]; + wallet: WalletUnlocked; + constructor(isConnected = true) { super(); events.addListener('request', this.onCommunicationMessage.bind(this)); @@ -34,6 +36,8 @@ export class MockConnection extends BaseConnection { this.addAsset, this.addAssets, ]); + this.assetsData = []; + this.wallet = Wallet.generate(generateOptions); this.isConnectedOverride = isConnected; } @@ -52,7 +56,8 @@ export class MockConnection extends BaseConnection { } async connect() { - return true; + this.isConnectedOverride = true; + return this.isConnectedOverride; } async disconnect() { @@ -67,7 +72,8 @@ export class MockConnection extends BaseConnection { return true; } - async addAssets(): Promise { + async addAssets(assets: Asset[]): Promise { + this.assetsData = assets; return true; } @@ -76,8 +82,7 @@ export class MockConnection extends BaseConnection { } async assets() { - const assets = await userWallet.getBalances(); - return assets; + return this.assetsData; } async onMessage() { @@ -89,11 +94,11 @@ export class MockConnection extends BaseConnection { } async accounts() { - return [userWallet.address.toAddress()]; + return [this.wallet.address.toAddress()]; } async signMessage(address: string, message: string) { - return userWallet.signMessage(message); + return this.wallet.signMessage(message); } async sendTransaction( @@ -101,12 +106,12 @@ export class MockConnection extends BaseConnection { _1: FuelProviderConfig, _2?: string | undefined ) { - const response = await userWallet.sendTransaction(transaction); + const response = await this.wallet.sendTransaction(transaction); return response.id; } async currentAccount() { - return userWallet.address.toAddress(); + return this.wallet.address.toAddress(); } async getWallet(address: string | AbstractAddress): Promise { diff --git a/packages/app/src/systems/Core/hooks/__mocks__/useWallet.ts b/packages/app/src/systems/Core/hooks/__mocks__/useWallet.ts index dbe0934c..f78b4d4f 100644 --- a/packages/app/src/systems/Core/hooks/__mocks__/useWallet.ts +++ b/packages/app/src/systems/Core/hooks/__mocks__/useWallet.ts @@ -5,8 +5,17 @@ import * as useWallet from '../useWallet'; import { MockConnection } from './MockConnection'; -export async function createWallet(isConnectedOverride = true) { +import { faucet } from '~/systems/Faucet/hooks/__mocks__/useFaucet'; +import { mint } from '~/systems/Mint/hooks/__mocks__/useMint'; +import { setAgreement, SWAYSWAP_ASSETS } from '~/systems/Welcome/machines'; + +export function createFuel(isConnectedOverride = true) { const mockFuel = MockConnection.start(isConnectedOverride); + return mockFuel as unknown as Fuel; +} + +export async function createWallet(isConnectedOverride = true) { + const mockFuel = createFuel(isConnectedOverride); const currentAccount = await mockFuel.currentAccount(); const wallet = await mockFuel.getWallet(currentAccount); return { wallet, fuel: mockFuel }; @@ -22,13 +31,29 @@ export function mockUseWallet(wallet: FuelWalletLocked) { }); } -export function mockUseFuel(fuel: MockConnection) { - window.fuel = fuel as unknown as Fuel; +export function mockUseFuel(fuel: Fuel) { + window.fuel = fuel; return jest.spyOn(useFuel, 'useFuel').mockImplementation(() => { return { - fuel: fuel as unknown as Fuel, + fuel, isLoading: false, error: '', }; }); } + +export async function mockUserData(opts: { faucetQuantity?: number } = {}) { + setAgreement(true); + const { wallet, fuel } = await createWallet(); + mockUseFuel(fuel); + mockUseWallet(wallet); + fuel.addAssets(SWAYSWAP_ASSETS); + await faucet(wallet, opts.faucetQuantity || 2); + await mint(wallet); + return { wallet, fuel }; +} + +export async function clearMockUserData() { + setAgreement(false); + window.fuel = createFuel(); +} diff --git a/packages/app/src/systems/Core/hooks/useWalletConnection.ts b/packages/app/src/systems/Core/hooks/useWalletConnection.ts new file mode 100644 index 00000000..0c734a56 --- /dev/null +++ b/packages/app/src/systems/Core/hooks/useWalletConnection.ts @@ -0,0 +1,28 @@ +import { useQuery } from 'react-query'; + +import { useFuel } from './useFuel'; + +export function useWalletConnection() { + const { fuel } = useFuel(); + const { + data: isConnected, + isLoading: isConnectedLoading, + isError: isConnectedError, + } = useQuery( + ['connected'], + async () => { + const isFuelConnected = await fuel!.isConnected(); + return isFuelConnected; + }, + { + enabled: !!fuel, + initialData: false, + refetchInterval: 1000, + } + ); + return { + isConnected, + isConnectedLoading, + isConnectedError, + }; +} diff --git a/packages/app/src/systems/Mint/routes.tsx b/packages/app/src/systems/Mint/routes.tsx index eabdb76c..969a2e77 100644 --- a/packages/app/src/systems/Mint/routes.tsx +++ b/packages/app/src/systems/Mint/routes.tsx @@ -8,7 +8,7 @@ import { Pages } from "~/types"; export const mintRoutes = ( diff --git a/packages/app/src/systems/Pool/pages/AddLiquidity.test.tsx b/packages/app/src/systems/Pool/pages/AddLiquidity.test.tsx index 1a6b43ae..d7f7ee3b 100644 --- a/packages/app/src/systems/Pool/pages/AddLiquidity.test.tsx +++ b/packages/app/src/systems/Pool/pages/AddLiquidity.test.tsx @@ -1,4 +1,4 @@ -import type { FuelWalletLocked } from "@fuel-wallet/sdk"; +import type { Fuel, FuelWalletLocked } from "@fuel-wallet/sdk"; import { screen, renderWithRouter, @@ -11,7 +11,6 @@ import * as poolQueries from "../utils/queries"; import { App } from "~/App"; import { ZERO } from "~/systems/Core"; -import type { MockConnection } from "~/systems/Core/hooks/__mocks__/MockConnection"; import { createWallet, mockUseFuel, @@ -19,16 +18,22 @@ import { } from "~/systems/Core/hooks/__mocks__/useWallet"; import { faucet } from "~/systems/Faucet/hooks/__mocks__/useFaucet"; import { mint } from "~/systems/Mint/hooks/__mocks__/useMint"; +import { setAgreement } from "~/systems/Welcome"; let wallet: FuelWalletLocked; -let fuel: MockConnection; +let fuel: Fuel; beforeAll(async () => { + setAgreement(true); ({ wallet, fuel } = await createWallet()); mockUseFuel(fuel); mockUseWallet(wallet); }); +afterAll(() => { + setAgreement(false); +}); + describe("Add Liquidity", () => { beforeEach(() => { mockUseUserPosition(); diff --git a/packages/app/src/systems/Pool/pages/Pools.test.tsx b/packages/app/src/systems/Pool/pages/Pools.test.tsx index 0db6a6e1..880fe74b 100644 --- a/packages/app/src/systems/Pool/pages/Pools.test.tsx +++ b/packages/app/src/systems/Pool/pages/Pools.test.tsx @@ -1,28 +1,11 @@ -import type { FuelWalletLocked } from "@fuel-wallet/sdk"; import { screen, renderWithRouter } from "@swayswap/test-utils"; -import { mockUseUserPosition } from "../hooks/__mocks__/useUserPosition"; - import { App } from "~/App"; -import type { MockConnection } from "~/systems/Core/hooks/__mocks__/MockConnection"; -import { - createWallet, - mockUseFuel, - mockUseWallet, -} from "~/systems/Core/hooks/__mocks__/useWallet"; - -let wallet: FuelWalletLocked; -let fuel: MockConnection; - -beforeAll(async () => { - ({ wallet, fuel } = await createWallet()); - mockUseWallet(wallet); - mockUseFuel(fuel); -}); +import { mockUserData } from "~/systems/Core/hooks/__mocks__/useWallet"; describe("Pool List", () => { beforeEach(() => { - mockUseUserPosition(); + mockUserData(); }); afterEach(() => { diff --git a/packages/app/src/systems/Pool/pages/RemoveLiquidity.test.tsx b/packages/app/src/systems/Pool/pages/RemoveLiquidity.test.tsx index 6a4360cb..400c66ef 100644 --- a/packages/app/src/systems/Pool/pages/RemoveLiquidity.test.tsx +++ b/packages/app/src/systems/Pool/pages/RemoveLiquidity.test.tsx @@ -1,4 +1,4 @@ -import type { FuelWalletLocked } from "@fuel-wallet/sdk"; +import type { Fuel, FuelWalletLocked } from "@fuel-wallet/sdk"; import { screen, renderWithRouter, @@ -15,23 +15,28 @@ import type { PoolInfoPreview } from "../utils"; import { App } from "~/App"; import { CONTRACT_ID } from "~/config"; import { COIN_ETH, ONE_ASSET, TOKENS } from "~/systems/Core"; -import type { MockConnection } from "~/systems/Core/hooks/__mocks__/MockConnection"; import { mockUseBalances } from "~/systems/Core/hooks/__mocks__/useBalances"; import { createWallet, mockUseFuel, mockUseWallet, } from "~/systems/Core/hooks/__mocks__/useWallet"; +import { setAgreement } from "~/systems/Welcome"; let wallet: FuelWalletLocked; -let fuel: MockConnection; +let fuel: Fuel; beforeAll(async () => { + setAgreement(true); ({ wallet, fuel } = await createWallet()); mockUseFuel(fuel); mockUseWallet(wallet); }); +afterAll(() => { + setAgreement(false); +}); + const USER_LIQUIDITY_POSITIONS: PoolInfoPreview = { ethReserve: bn("1009199438931"), formattedEthReserve: "1,009.199", diff --git a/packages/app/src/systems/Swap/pages/SwapPage.test.tsx b/packages/app/src/systems/Swap/pages/SwapPage.test.tsx index 70a712a0..e659ef29 100644 --- a/packages/app/src/systems/Swap/pages/SwapPage.test.tsx +++ b/packages/app/src/systems/Swap/pages/SwapPage.test.tsx @@ -12,15 +12,11 @@ import * as poolHelpers from "../../Pool/utils/helpers"; import * as swapHelpers from "../utils/helpers"; import { App } from "~/App"; -import { TOKENS } from "~/systems/Core"; import { - createWallet, - mockUseFuel, - mockUseWallet, + clearMockUserData, + mockUserData, } from "~/systems/Core/hooks/__mocks__/useWallet"; import { faucet } from "~/systems/Faucet/hooks/__mocks__/useFaucet"; -import { mint } from "~/systems/Mint/hooks/__mocks__/useMint"; -import { addLiquidity } from "~/systems/Pool/hooks/__mocks__/addLiquidity"; async function findSwapBtn() { return waitFor(() => screen.findByLabelText(/swap button/i)); @@ -38,13 +34,6 @@ async function clickOnMaxBalance(input: "from" | "to" = "from") { }); } -async function createAndMockWallet() { - const { wallet, fuel } = await createWallet(); - mockUseFuel(fuel); - mockUseWallet(wallet); - return wallet; -} - async function fillCoinFromWithValue(value: string) { await waitFor( async () => { @@ -86,9 +75,14 @@ describe("SwapPage", () => { let wallet: FuelWalletLocked; beforeAll(async () => { - wallet = await createAndMockWallet(); - await faucet(wallet, 4); - await mint(wallet); + const mocks = await mockUserData({ + faucetQuantity: 4, + }); + wallet = mocks.wallet; + }, 1000 * 60); + + afterAll(() => { + clearMockUserData(); }); describe("without liquidity", () => { @@ -157,14 +151,14 @@ describe("SwapPage", () => { describe("with liquidity created", () => { beforeAll(async () => { - await faucet(wallet, 4); - await addLiquidity( - wallet, - "0.5", - "500", - TOKENS[0].assetId, - TOKENS[1].assetId - ); + const mocks = await mockUserData({ + faucetQuantity: 4, + }); + wallet = mocks.wallet; + }, 1000 * 60); + + afterAll(() => { + clearMockUserData(); }); it("should show insufficient balance if try to input more than balance", async () => { diff --git a/packages/app/src/systems/Welcome/components/AddAssets.tsx b/packages/app/src/systems/Welcome/components/AddAssets.tsx index ffca8175..4468dec2 100644 --- a/packages/app/src/systems/Welcome/components/AddAssets.tsx +++ b/packages/app/src/systems/Welcome/components/AddAssets.tsx @@ -6,14 +6,14 @@ import { WelcomeImage } from "./WelcomeImage"; import { WelcomeStep } from "./WelcomeStep"; export function AddAssets() { - const { service, state } = useWelcomeSteps(); + const { send, state } = useWelcomeSteps(); function handleAddAssets() { - service.send("ADD_ASSETS"); + send("ADD_ASSETS"); } return ( - +

Add SwaySwap assets

@@ -24,7 +24,7 @@ export function AddAssets() { diff --git a/packages/app/src/systems/Welcome/components/AddFunds.tsx b/packages/app/src/systems/Welcome/components/AddFunds.tsx index 39dcae89..718c3d23 100644 --- a/packages/app/src/systems/Welcome/components/AddFunds.tsx +++ b/packages/app/src/systems/Welcome/components/AddFunds.tsx @@ -14,7 +14,7 @@ export function AddFunds() { ); return ( - +

Add some test ETH to your wallet

diff --git a/packages/app/src/systems/Welcome/components/MintAssets.tsx b/packages/app/src/systems/Welcome/components/MintAssets.tsx index e385ed76..f231704e 100644 --- a/packages/app/src/systems/Welcome/components/MintAssets.tsx +++ b/packages/app/src/systems/Welcome/components/MintAssets.tsx @@ -6,14 +6,14 @@ import { WelcomeImage } from "./WelcomeImage"; import { WelcomeStep } from "./WelcomeStep"; export function MintAssets() { - const { service, state } = useWelcomeSteps(); + const { send, state } = useWelcomeSteps(); function handleAddAssets() { - service.send("ADD_ASSETS"); + send("MINT_ASSETS"); } return ( - +

Add some test Assets to your wallet

@@ -25,7 +25,7 @@ export function MintAssets() { diff --git a/packages/app/src/systems/Welcome/components/WelcomeConnect.tsx b/packages/app/src/systems/Welcome/components/WelcomeConnect.tsx index 84c2fada..eb9b2caa 100644 --- a/packages/app/src/systems/Welcome/components/WelcomeConnect.tsx +++ b/packages/app/src/systems/Welcome/components/WelcomeConnect.tsx @@ -1,42 +1,20 @@ import { Button } from "@fuel-ui/react"; -import { useMutation } from "react-query"; +import { useSelector } from "@xstate/react"; import { useWelcomeSteps } from "../hooks"; import { WelcomeImage } from "./WelcomeImage"; import { WelcomeStep } from "./WelcomeStep"; -import { useFuel } from "~/systems/Core/hooks/useFuel"; - export const WelcomeConnect = () => { - const { next } = useWelcomeSteps(); - const { fuel } = useFuel(); - - const connectWalletMutation = useMutation( - async () => { - if (!fuel) { - throw new Error( - "Trying to connect wallet when fuel instance is not injected" - ); - } - await fuel.connect(); - }, - { - onSuccess: () => { - next(); - }, - } - ); - - function handleConnectWallet() { - connectWalletMutation.mutate(); - } + const { service, send } = useWelcomeSteps(); + const installWallet = useSelector(service, (s) => s.matches("installWallet")); return ( - +

Welcome to SwaySwap

- {!fuel ? ( + {installWallet ? ( <>

To get started you'll need to install @@ -64,7 +42,7 @@ export const WelcomeConnect = () => { variant="solid" size="lg" className="mt-5 mx-auto" - onPress={handleConnectWallet} + onPress={() => send("CONNECT")} > Connect Wallet diff --git a/packages/app/src/systems/Welcome/components/WelcomeStep.tsx b/packages/app/src/systems/Welcome/components/WelcomeStep.tsx index 24ebdef8..25af6c56 100644 --- a/packages/app/src/systems/Welcome/components/WelcomeStep.tsx +++ b/packages/app/src/systems/Welcome/components/WelcomeStep.tsx @@ -1,23 +1,12 @@ -import { useSelector } from "@xstate/react"; import type { ReactNode } from "react"; -import { Navigate } from "react-router-dom"; - -import { useWelcomeSteps, stepsSelectors } from "../hooks"; import { AnimatedPage } from "~/systems/Core"; type WelcomeStepProps = { - id: number; children: ReactNode; }; -export function WelcomeStep({ id, children }: WelcomeStepProps) { - const { service } = useWelcomeSteps(); - const current = useSelector(service, stepsSelectors.current); - if (current?.id < id) { - return ; - } - +export function WelcomeStep({ children }: WelcomeStepProps) { return (

{children}
diff --git a/packages/app/src/systems/Welcome/components/WelcomeDone.tsx b/packages/app/src/systems/Welcome/components/WelcomeTerms.tsx similarity index 79% rename from packages/app/src/systems/Welcome/components/WelcomeDone.tsx rename to packages/app/src/systems/Welcome/components/WelcomeTerms.tsx index f7d32de5..b67be538 100644 --- a/packages/app/src/systems/Welcome/components/WelcomeDone.tsx +++ b/packages/app/src/systems/Welcome/components/WelcomeTerms.tsx @@ -1,3 +1,5 @@ +import { useState } from "react"; + import { useWelcomeSteps } from "../hooks"; import { WelcomeImage } from "./WelcomeImage"; @@ -8,15 +10,16 @@ import { Button, Link } from "~/systems/UI"; const DISCLAIMER_URL = "https://github.com/FuelLabs/swayswap/blob/master/docs/LEGAL_DISCLAIMER.md"; -export function WelcomeDone() { - const { send, state } = useWelcomeSteps(); +export function WelcomeTerms() { + const { send } = useWelcomeSteps(); + const [agreement, setAgreement] = useState(false); function handleDone() { - send("FINISH"); + send("ACCEPT_AGREEMENT"); } return ( - +

This is running on the Fuel test network. No real funds are used. @@ -27,11 +30,9 @@ export function WelcomeDone() { { - send("ACCEPT_AGREEMENT", { - value: e.target.checked, - }); + setAgreement(e.target.checked); }} className="h-5 w-5 mr-1 cursor-pointer" type="checkbox" @@ -47,7 +48,7 @@ export function WelcomeDone() { size="lg" variant="primary" className="mt-5 mx-auto" - isDisabled={!state.context.acceptAgreement} + isDisabled={!agreement} onPress={handleDone} > Get Swapping! diff --git a/packages/app/src/systems/Welcome/components/index.tsx b/packages/app/src/systems/Welcome/components/index.tsx index 2d1611bb..ea1bd636 100644 --- a/packages/app/src/systems/Welcome/components/index.tsx +++ b/packages/app/src/systems/Welcome/components/index.tsx @@ -1,7 +1,7 @@ export * from "./AddAssets"; export * from "./AddFunds"; export * from "./StepsIndicator"; -export * from "./WelcomeDone"; +export * from "./WelcomeTerms"; export * from "./WelcomeNavItem"; export * from "./WelcomeSidebar"; export * from "./WelcomeSidebarBullet"; diff --git a/packages/app/src/systems/Welcome/hooks/useWelcomeSteps.tsx b/packages/app/src/systems/Welcome/hooks/useWelcomeSteps.tsx index 501c76c4..ad0051fe 100644 --- a/packages/app/src/systems/Welcome/hooks/useWelcomeSteps.tsx +++ b/packages/app/src/systems/Welcome/hooks/useWelcomeSteps.tsx @@ -1,36 +1,23 @@ -import { useMachine } from "@xstate/react"; -import type { BN } from "fuels"; -import { bn } from "fuels"; +import { useInterpret, useSelector } from "@xstate/react"; import type { ReactNode } from "react"; -import { useContext, createContext } from "react"; +import { useEffect, useContext, createContext } from "react"; import { useNavigate } from "react-router-dom"; -import type { InterpreterFrom, StateFrom } from "xstate"; -import { assign, createMachine } from "xstate"; +import type { StateFrom } from "xstate"; -import { - handleError, - ETH, - DAI, - ETH_DAI, - LocalStorageKey, -} from "~/systems/Core"; -import { getOverrides } from "~/systems/Core/utils/gas"; +import type { + WelcomeMachine, + WelcomeMachineService, + WelcomeMachineState, +} from "../machines"; +import { welcomeMachine } from "../machines"; + +import { LocalStorageKey } from "~/systems/Core"; +import { useFuel } from "~/systems/Core/hooks/useFuel"; import type { Maybe } from "~/types"; import { Pages } from "~/types"; -import { TokenContractAbi__factory } from "~/types/contracts"; -export const LOCALSTORAGE_WELCOME_KEY = `${LocalStorageKey}fuel--welcomeStep`; export const LOCALSTORAGE_AGREEMENT_KEY = `${LocalStorageKey}fuel--agreement`; -export const STEPS = [ - { id: 0, path: Pages.connect }, - { id: 1, path: Pages.faucet }, - { id: 2, path: Pages.addAssets }, - { id: 3, path: Pages.mint }, - { id: 4, path: Pages["welcome.done"] }, - { id: 5, path: null }, -]; - export function getAgreement() { return localStorage.getItem(LOCALSTORAGE_AGREEMENT_KEY) === "true"; } @@ -39,31 +26,6 @@ export function setAgreement(accept: boolean) { localStorage.setItem(LOCALSTORAGE_AGREEMENT_KEY, String(accept)); } -export function getCurrent() { - try { - const curr = localStorage.getItem(LOCALSTORAGE_WELCOME_KEY); - - return curr ? JSON.parse(curr) : STEPS[0]; - } catch (_) { - return STEPS[0]; - } -} - -export function setCurrent(id: number) { - const current = STEPS[id]; - localStorage.setItem(LOCALSTORAGE_WELCOME_KEY, JSON.stringify(current)); - return current; -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function assignCurrent(id: number): any { - return assign({ - current: (_) => { - return setCurrent(id); - }, - }); -} - // ---------------------------------------------------------------------------- // State Machine // ---------------------------------------------------------------------------- @@ -73,341 +35,62 @@ export type Step = { path: Maybe; }; -type MachineContext = { - current: Step; - acceptAgreement: boolean; - balance: BN; -}; - -type MachineEvents = { type: "NEXT" } | { type: "SET_CURRENT"; value: number }; - -type MachineServices = { - fetchBalance: { - data: Array; - }; -}; - -const welcomeStepsMachine = - /** @xstate-layout N4IgpgJg5mDOIC5QHcwBsDGB7AtmAygC5gAOsAdAJYB2lhAxANoAMAuoqCVrHZVtRxAAPRAEYAzAFZyAFgCcCgOyTRc8QDZF65gCYANCACeiGeLnkAHDouiVOxXNWTxigL6uDqTLgLEyVWgZGUXYkEC4eQj4BMJEECWl5JRU1TW19IzE5HXJmRRtxC3EE9XUZd090bDwiUgoaOiYdUM5uXn5BOITZBTllVQ0tXQNjBHEZRUtrURlJPtEHdQt1CpAvat86gMbGcRbwtqiO2LEpHuSBtOHMsbUpmxllnUl5dVFV9Z9a-2xqajAMFFqFAAOoAQzQaDADAAcgBRAAaABUWPsIu0YqA4vJpFpJJJFIpbHJHqVFCMxBZzFYCopiqIdAtsh8ql8-BQIPwwPQAIIAYT5cIACkiAPo8gDiACU4XCALJwmEotiCdFHTHCRBzGTkHRJTRUuZ9CkIGzkSTMS3MUw6cR5Kws7w1dnkTn-egAMQAkjCvfgABKo1WHaKdRAFciiBaidTicSMtTZE2MizkOTOGZRmTMOPFNyrahYCBwQSfZ11YORUMnBAAWnUJvruStLdbeUdG2+9UClYxYYQMgyo3G5myDxeUmsFg7bK2v3+gJooIhUMIvfV-btOTjc2eMnUjijJts4nuElUVlEFnx5Q8a1Z5f8brA6+rWMQ2nUaaWdL6sfyObHlYaYZlINgSJa053mWmz+AAZg0sAABaQK+xzvggajSDG2YMn+khWEOiCMpM6bFIM2TMLYEzuO4QA */ - createMachine( - { - id: "welcomeSteps", - predictableActionArguments: true, - initial: "init", - schema: { - context: {} as MachineContext, - events: {} as MachineEvents, - services: {} as MachineServices, - }, - context: { - current: getCurrent(), - acceptAgreement: getAgreement(), - balance: bn(), - }, - states: { - init: { - always: [ - /** - * This is mainly used for tests purposes - */ - { - target: "connectingWallet", - cond: (ctx) => { - return ctx.current.id === 0; - }, - }, - { - target: "fauceting", - cond: (ctx) => { - return ctx.current.id === 1; - }, - }, - { - target: "addingAssets", - cond: (ctx) => { - return ctx.current.id === 2; - }, - }, - { - target: "mintingAssets", - cond: (ctx) => { - return ctx.current.id === 3; - }, - }, - { - target: "done", - cond: (ctx) => - ctx.current.id === 4 || - (ctx.current.id >= 4 && !ctx.acceptAgreement), - }, - { - cond: (ctx) => ctx.current.id === 5 && ctx.acceptAgreement, - target: "finished", - }, - ], - }, - connectingWallet: { - entry: [assignCurrent(0), "navigateTo"], - on: { - NEXT: { - target: "fecthingBalance", - }, - }, - }, - fecthingBalance: { - invoke: { - src: "fetchBalance", - onDone: [ - { - cond: "hasNoBalance", - actions: ["assignBalances"], - target: "fauceting", - }, - { - actions: ["assignBalances"], - target: "addingAssets", - }, - ], - onError: { - actions: ["toastErrorMessage"], - }, - }, - }, - fauceting: { - entry: [assignCurrent(1), "navigateTo"], - on: { - NEXT: { - target: "#welcomeSteps.addingAssets", - }, - }, - }, - addingAssets: { - entry: [assignCurrent(2), "navigateTo"], - initial: "addAssetsToWallet", - states: { - addAssetsToWallet: { - on: { - ADD_ASSETS: { - target: "addingAssets", - }, - }, - }, - addingAssets: { - tags: ["isLoadingMint"], - invoke: { - src: "addAssets", - onDone: "#welcomeSteps.mintingAssets", - onError: { - actions: ["toastErrorMessage"], - target: "addAssetsToWallet", - }, - }, - }, - }, - }, - mintingAssets: { - entry: [assignCurrent(3), "navigateTo"], - initial: "mintAssets", - states: { - mintAssets: { - on: { - ADD_ASSETS: { - target: "mintingAssets", - }, - }, - }, - mintingAssets: { - tags: ["isLoadingMint"], - invoke: { - src: "mintAssets", - onDone: "#welcomeSteps.done", - onError: { - actions: ["toastErrorMessage"], - target: "mintAssets", - }, - }, - }, - }, - }, - done: { - entry: [assignCurrent(4), "navigateTo"], - on: { - ACCEPT_AGREEMENT: { - actions: ["acceptAgreement"], - }, - FINISH: { - target: "finished", - }, - }, - }, - finished: { - entry: assignCurrent(5), - type: "final", - }, - }, - }, - { - actions: { - assignBalances: assign({ - balance: (_, ev) => ev.data, - }), - toastErrorMessage(_, ev) { - handleError(ev.data); - // eslint-disable-next-line no-console - console.error(ev.data); - }, - }, - guards: { - hasNoBalance: (_, ev) => { - return bn(ev.data).isZero(); - }, - }, - services: { - fetchBalance: async () => { - if (!window.fuel) { - throw new Error("Fuel Wallet is not detected!"); - } - const [address] = await window.fuel.accounts(); - if (!address) { - throw Error("No account found!"); - } - const wallet = await window.fuel.getWallet(address); - return wallet.getBalance(); - }, - addAssets: async () => { - if (!window.fuel) { - throw new Error("Fuel Wallet is not detected!"); - } - const assetsOnWallet = await window.fuel.assets(); - const assetsOnWalletIds = assetsOnWallet.map((a) => a.assetId); - const assetsToAddToWallet = [ - { - assetId: ETH.assetId, - name: "sEther", - symbol: "sETH", - imageUrl: ETH.img, - isCustom: true, - }, - { - assetId: DAI.assetId, - name: "Dai", - symbol: "Dai", - imageUrl: DAI.img, - isCustom: true, - }, - { - assetId: ETH_DAI.assetId, - name: ETH_DAI.name, - symbol: ETH_DAI.symbol, - imageUrl: ETH_DAI.img, - isCustom: true, - }, - ]; - const assets = assetsToAddToWallet.filter( - (a) => !assetsOnWalletIds.includes(a.assetId) - ); - if (assets.length !== 0) { - await window.fuel.addAssets(assets); - } - }, - mintAssets: async () => { - if (!window.fuel) { - throw new Error("Fuel Wallet is not detected!"); - } - const [address] = await window.fuel.accounts(); - if (!address) { - throw Error("No account found!"); - } - const wallet = await window.fuel.getWallet(address); - const token1 = TokenContractAbi__factory.connect(ETH.assetId, wallet); - const token2 = TokenContractAbi__factory.connect(DAI.assetId, wallet); - const calls = []; - - const addressId = { - value: wallet.address.toHexString(), - }; - const { value: hasMint1 } = await token1.functions - .has_mint(addressId) - .get(); - if (!hasMint1) { - calls.push(token1.functions.mint()); - } - const { value: hasMint2 } = await token2.functions - .has_mint(addressId) - .get(); - if (!hasMint2) { - calls.push(token2.functions.mint()); - } - - if (calls.length === 0) { - return; - } - - await token1.multiCall(calls).txParams(getOverrides()).call(); - }, - }, - } - ); - -// ---------------------------------------------------------------------------- -// Context & Provider -// ---------------------------------------------------------------------------- - -type Machine = typeof welcomeStepsMachine; -type Service = InterpreterFrom; -type Context = { - state: StateFrom; - send: Service["send"]; - service: Service; - next: () => void; -}; type WelcomeStepsProviderProps = { children: ReactNode; }; export const stepsSelectors = { - current(state: StateFrom) { + current(state: StateFrom) { return state.context.current; }, - isFinished(state: StateFrom) { + isFinished(state: StateFrom) { return state.matches("finished"); }, }; +type Context = { + state: WelcomeMachineState; + send: WelcomeMachineService["send"]; + service: WelcomeMachineService; + next: () => void; +}; + const ctx = createContext({} as Context); -export function StepsProvider({ children }: WelcomeStepsProviderProps) { - const navigate = useNavigate(); - const [state, send, service] = useMachine(() => - welcomeStepsMachine - .withContext({ - current: getCurrent(), - acceptAgreement: getAgreement(), - balance: bn(), - }) - .withConfig({ - actions: { - navigateTo: (context) => { - if (context.current.id > 4) return; - navigate(`/welcome/${context.current.path}`); - }, - acceptAgreement: assign((context, event) => { - setAgreement(event.value); - return { - ...context, - acceptAgreement: event.value, - }; - }), +export function WelcomeStepsProvider({ children }: WelcomeStepsProviderProps) { + const navigate = useNavigate(); + const { fuel } = useFuel(); + const service = useInterpret(() => + welcomeMachine.withConfig({ + actions: { + navigateTo: (context) => { + if (!context.current.path) { + navigate(Pages.swap); + return; + } + navigate(`/welcome/${context.current.path}`, { replace: true }); }, - }) + }, + }) ); + const send = service.send; + const state = useSelector(service, (s) => s); + + useEffect(() => { + if (fuel) { + service.send({ + type: "WALLET_DETECTED", + value: fuel, + }); + } + }, [fuel]); function next() { send("NEXT"); } return ( - + {children} ); diff --git a/packages/app/src/systems/Welcome/machines/index.ts b/packages/app/src/systems/Welcome/machines/index.ts new file mode 100644 index 00000000..977c1aa9 --- /dev/null +++ b/packages/app/src/systems/Welcome/machines/index.ts @@ -0,0 +1 @@ +export * from './welcomeMachine'; diff --git a/packages/app/src/systems/Welcome/machines/welcomeMachine.test.ts b/packages/app/src/systems/Welcome/machines/welcomeMachine.test.ts new file mode 100644 index 00000000..64bf23de --- /dev/null +++ b/packages/app/src/systems/Welcome/machines/welcomeMachine.test.ts @@ -0,0 +1,200 @@ +import { bn } from 'fuels'; +import { interpret } from 'xstate'; +import { waitFor } from 'xstate/lib/waitFor'; + +import type { WelcomeMachineService } from './welcomeMachine'; +import { setAgreement, SWAYSWAP_ASSETS, STEPS, welcomeMachine } from './welcomeMachine'; + +import { createFuel } from '~/systems/Core/hooks/__mocks__/useWallet'; +import { DAI, ETH } from '~/systems/Core/utils/tokenList'; +import { faucet } from '~/systems/Faucet/hooks/__mocks__/useFaucet'; +import { TokenContractAbi__factory } from '~/types/contracts'; + +describe('captchaMachine', () => { + let service: WelcomeMachineService; + + beforeAll(() => { + setAgreement(false); + }); + + beforeEach(() => { + setAgreement(false); + }); + + beforeEach(() => { + service = interpret( + welcomeMachine + .withContext({ + acceptAgreement: false, + balance: bn(), + current: STEPS[0], + }) + .withConfig({ + actions: { + navigateTo: () => {}, + acceptAgreement: () => {}, + }, + }) + ).start(); + }); + + afterEach(() => { + service.stop(); + }); + + it('Should check if wallet is installed', async () => { + const state = await waitFor(service, (s) => s.matches('installWallet')); + expect(state.matches('installWallet')).toBeTruthy(); + + service.send('WALLET_DETECTED', { + value: createFuel(false), + }); + + const state2 = await waitFor(service, (s) => s.matches('connectingWallet')); + expect(state2.matches('connectingWallet')).toBeTruthy(); + }); + + it('Should go to connecting if Wallet is detected', async () => { + const service2 = interpret( + welcomeMachine + .withContext({ + acceptAgreement: false, + balance: bn(), + current: STEPS[0], + fuel: createFuel(false), + }) + .withConfig({ + actions: { + navigateTo: () => {}, + acceptAgreement: () => {}, + }, + }) + ).start(); + const state2 = await waitFor(service2, (s) => s.matches('connectingWallet')); + expect(state2.matches('connectingWallet')).toBeTruthy(); + }); + + it('Should connect to wallet', async () => { + service.send('WALLET_DETECTED', { + value: createFuel(false), + }); + await waitFor(service, (s) => s.matches('connectingWallet')); + service.send('CONNECT'); + await waitFor(service, (s) => s.matches('connectingWallet.connecting')); + await waitFor(service, (s) => s.matches('fecthingBalance')); + }); + + it('Should ask for faucet', async () => { + const fuel = createFuel(false); + service.send('WALLET_DETECTED', { + value: fuel, + }); + await waitFor(service, (s) => s.matches('connectingWallet')); + service.send('CONNECT'); + await waitFor(service, (s) => s.matches('connectingWallet.connecting')); + await waitFor(service, (s) => s.matches('fecthingBalance')); + await waitFor(service, (s) => s.matches('fauceting')); + const account = await fuel.currentAccount(); + const wallet = await fuel.getWallet(account); + await faucet(wallet); + service.send('NEXT'); + await waitFor(service, (s) => s.matches('addingAssets')); + }); + + it('Should add assets to wallet', async () => { + const fuel = createFuel(true); + const account = await fuel.currentAccount(); + const wallet = await fuel.getWallet(account); + await faucet(wallet); + + service.send('WALLET_DETECTED', { + value: fuel, + }); + await waitFor(service, (s) => s.matches('addingAssets.addAssetsToWallet')); + service.send('ADD_ASSETS'); + await waitFor(service, (s) => s.matches('addingAssets.addingAssets')); + await waitFor(service, (s) => s.matches('mintingAssets')); + }); + + it('Should mint assets to wallet', async () => { + const fuel = createFuel(true); + const account = await fuel.currentAccount(); + const wallet = await fuel.getWallet(account); + fuel.addAssets(SWAYSWAP_ASSETS); + await faucet(wallet); + service.send('WALLET_DETECTED', { + value: fuel, + }); + await waitFor(service, (s) => s.matches('mintingAssets.mintAssets')); + service.send('MINT_ASSETS'); + await waitFor(service, (s) => s.matches('mintingAssets.mintingAssets')); + await waitFor(service, (s) => s.matches('acceptAgreement')); + }); + + it( + 'Should accept aggrement', + async () => { + const fuel = createFuel(true); + const account = await fuel.currentAccount(); + const wallet = await fuel.getWallet(account); + fuel.addAssets(SWAYSWAP_ASSETS); + await faucet(wallet); + const token1 = TokenContractAbi__factory.connect(ETH.assetId, wallet); + const token2 = TokenContractAbi__factory.connect(DAI.assetId, wallet); + await token1 + .multiCall([token1.functions.mint(), token2.functions.mint()]) + .txParams({ + gasPrice: 1, + }) + .call(); + + service.send('WALLET_DETECTED', { + value: fuel, + }); + await waitFor(service, (s) => s.matches('acceptAgreement')); + service.send('ACCEPT_AGREEMENT'); + await waitFor(service, (s) => s.matches('finished')); + }, + 1000 * 60 * 2 + ); + + it( + 'Should go to finish if all steps are complete', + async () => { + const fuel = createFuel(true); + const service2 = interpret( + welcomeMachine + .withContext({ + acceptAgreement: true, + balance: bn(), + current: STEPS[0], + }) + .withConfig({ + actions: { + navigateTo: () => {}, + acceptAgreement: () => true, + }, + }) + ).start(); + const account = await fuel.currentAccount(); + const wallet = await fuel.getWallet(account); + fuel.addAssets(SWAYSWAP_ASSETS); + await faucet(wallet); + const token1 = TokenContractAbi__factory.connect(ETH.assetId, wallet); + const token2 = TokenContractAbi__factory.connect(DAI.assetId, wallet); + await token1 + .multiCall([token1.functions.mint(), token2.functions.mint()]) + .txParams({ + gasPrice: 1, + }) + .call(); + + service2.send('WALLET_DETECTED', { + value: fuel, + }); + + await waitFor(service2, (s) => s.matches('finished')); + }, + 1000 * 60 * 2 + ); +}); diff --git a/packages/app/src/systems/Welcome/machines/welcomeMachine.ts b/packages/app/src/systems/Welcome/machines/welcomeMachine.ts new file mode 100644 index 00000000..ca6a18a5 --- /dev/null +++ b/packages/app/src/systems/Welcome/machines/welcomeMachine.ts @@ -0,0 +1,462 @@ +import type { Fuel } from '@fuel-wallet/sdk'; +import type { BN } from 'fuels'; +import { bn } from 'fuels'; +import type { InterpreterFrom, StateFrom } from 'xstate'; +import { assign, createMachine } from 'xstate'; + +import { handleError, LocalStorageKey } from '~/systems/Core'; +import { getOverrides } from '~/systems/Core/utils/gas'; +import { ETH, DAI, ETH_DAI } from '~/systems/Core/utils/tokenList'; +import type { Maybe } from '~/types'; +import { Pages } from '~/types'; +import { TokenContractAbi__factory } from '~/types/contracts'; + +export const LOCALSTORAGE_WELCOME_KEY = `${LocalStorageKey}fuel--welcomeStep`; +export const LOCALSTORAGE_AGREEMENT_KEY = `${LocalStorageKey}fuel--agreement`; + +export const STEPS = [ + { id: 0, path: Pages.welcomeConnect }, + { id: 1, path: Pages.welcomeFaucet }, + { id: 2, path: Pages.welcomeAddAssets }, + { id: 3, path: Pages.welcomeMint }, + { id: 4, path: Pages.welcomeTerms }, + { id: 5, path: null }, +]; + +export const SWAYSWAP_ASSETS = [ + { + assetId: ETH.assetId, + name: 'sEther', + symbol: 'sETH', + imageUrl: ETH.img, + isCustom: true, + }, + { + assetId: DAI.assetId, + name: 'Dai', + symbol: 'Dai', + imageUrl: DAI.img, + isCustom: true, + }, + { + assetId: ETH_DAI.assetId, + name: ETH_DAI.name, + symbol: ETH_DAI.symbol, + imageUrl: ETH_DAI.img, + isCustom: true, + }, +]; + +export function getAgreement() { + return localStorage.getItem(LOCALSTORAGE_AGREEMENT_KEY) === 'true'; +} + +export function setAgreement(accept: boolean) { + localStorage.setItem(LOCALSTORAGE_AGREEMENT_KEY, String(accept)); +} + +export function setCurrent(id: number) { + const current = STEPS[id]; + return current; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function assignCurrent(id: number): any { + return assign({ + current: (_) => { + return STEPS[id]; + }, + }); +} + +// ---------------------------------------------------------------------------- +// State Machine +// ---------------------------------------------------------------------------- + +export type Step = { + id: number; + path: Maybe; +}; + +type MachineContext = { + fuel?: Fuel; + current: Step; + acceptAgreement: boolean; + balance: BN; +}; + +type MachineEvents = + | { type: 'NEXT' } + | { type: 'SET_CURRENT'; value: number } + | { type: 'WALLET_DETECTED'; value: Fuel } + | { type: 'ADD_ASSETS' } + | { type: 'MINT_ASSETS' } + | { type: 'ACCEPT_AGREEMENT' } + | { type: 'CONNECT' }; + +type MachineServices = { + fetchBalance: { + data: BN; + }; + addAssets: { + data: void; + }; + mintAssets: { + data: void; + }; + fetchMintConditions: { + data: boolean[]; + }; + fetchAssets: { + data: Array; + }; +}; + +export type CreateWelcomeMachineConfig = { + context?: Partial; + actions?: { + navigateTo: (ctx: MachineContext) => void; + acceptAgreement: (ctx: MachineContext) => void; + }; +}; + +export const welcomeMachine = createMachine( + { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + tsTypes: {} as import('./welcomeMachine.typegen').Typegen0, + id: 'welcomeSteps', + predictableActionArguments: true, + schema: { + context: {} as MachineContext, + events: {} as MachineEvents, + services: {} as MachineServices, + }, + context: { + current: STEPS[0], + acceptAgreement: getAgreement(), + balance: bn(), + }, + initial: 'installWallet', + states: { + installWallet: { + entry: [assignCurrent(0), 'navigateTo'], + always: [ + { + cond: (ctx) => !!ctx.fuel, + target: 'connectingWallet', + }, + ], + on: { + WALLET_DETECTED: { + actions: ['assignFuel'], + target: 'connectingWallet', + }, + }, + }, + connectingWallet: { + entry: [assignCurrent(0), 'navigateTo'], + initial: 'fetchingConnection', + states: { + idle: { + on: { + CONNECT: { + target: 'connecting', + }, + }, + }, + fetchingConnection: { + tags: ['isLoading'], + invoke: { + src: (ctx) => ctx.fuel!.isConnected(), + onDone: [ + { + cond: (_, ev) => ev.data, + target: '#welcomeSteps.fecthingBalance', + }, + { + target: 'idle', + }, + ], + onError: { + actions: ['toastErrorMessage'], + target: 'idle', + }, + }, + }, + connecting: { + tags: ['isLoading'], + invoke: { + src: (ctx) => ctx.fuel!.connect(), + onDone: [ + { + cond: (_, ev) => ev.data, + target: '#welcomeSteps.fecthingBalance', + }, + { + target: 'idle', + }, + ], + onError: { + actions: ['toastErrorMessage'], + target: 'idle', + }, + }, + }, + }, + }, + fecthingBalance: { + tags: ['isLoading'], + invoke: { + src: 'fetchBalance', + onDone: [ + { + cond: 'hasNoBalance', + actions: ['assignBalances'], + target: 'fauceting', + }, + { + actions: ['assignBalances'], + target: 'addingAssets', + }, + ], + onError: { + actions: ['toastErrorMessage'], + }, + }, + }, + fauceting: { + entry: [assignCurrent(1), 'navigateTo'], + on: { + NEXT: { + target: '#welcomeSteps.addingAssets', + }, + }, + }, + addingAssets: { + entry: [assignCurrent(2), 'navigateTo'], + initial: 'fetchingConditions', + states: { + fetchingConditions: { + tags: ['isLoading'], + invoke: { + src: 'fetchAssets', + onDone: [ + { + cond: 'hasAssets', + target: '#welcomeSteps.mintingAssets', + }, + { + target: 'addAssetsToWallet', + }, + ], + }, + }, + addAssetsToWallet: { + on: { + ADD_ASSETS: { + target: 'addingAssets', + }, + }, + }, + addingAssets: { + tags: ['isLoading'], + invoke: { + src: 'addAssets', + onDone: '#welcomeSteps.mintingAssets', + onError: { + actions: ['toastErrorMessage'], + target: 'addAssetsToWallet', + }, + }, + }, + }, + }, + mintingAssets: { + entry: [assignCurrent(3), 'navigateTo'], + initial: 'fetchingMintConditions', + states: { + fetchingMintConditions: { + tags: ['isLoading'], + invoke: { + src: 'fetchMintConditions', + onDone: [ + { + cond: 'hasMintedAssets', + target: '#welcomeSteps.acceptAgreement', + }, + { + target: 'mintAssets', + }, + ], + }, + }, + mintAssets: { + on: { + MINT_ASSETS: { + target: 'mintingAssets', + }, + }, + }, + mintingAssets: { + tags: ['isLoading'], + invoke: { + src: 'mintAssets', + onDone: '#welcomeSteps.acceptAgreement', + onError: { + actions: ['toastErrorMessage'], + target: 'mintAssets', + }, + }, + }, + }, + }, + acceptAgreement: { + entry: [assignCurrent(4), 'navigateTo'], + always: [ + { + cond: (ctx) => ctx.acceptAgreement, + target: 'finished', + }, + ], + on: { + ACCEPT_AGREEMENT: { + actions: ['acceptAgreement'], + target: 'finished', + }, + }, + }, + finished: { + entry: [assignCurrent(5), 'navigateTo'], + type: 'final', + }, + }, + }, + { + actions: { + assignBalances: assign({ + balance: (_, ev) => ev.data, + }), + toastErrorMessage(_, ev) { + handleError(ev.data); + // eslint-disable-next-line no-console + console.error(ev.data); + }, + assignFuel: assign({ + fuel: (_, ev) => ev.value, + }), + navigateTo: () => {}, + acceptAgreement: assign({ + acceptAgreement: () => { + setAgreement(true); + return true; + }, + }), + }, + guards: { + hasNoBalance: (_, ev) => bn(ev.data).isZero(), + hasAssets: (_, ev) => { + const items = SWAYSWAP_ASSETS.filter((a) => ev.data.includes(a.assetId)); + return items.length === SWAYSWAP_ASSETS.length; + }, + hasMintedAssets: (_, ev) => { + const items = ev.data.filter((i) => i); + // 2 is the number of assets that can be minted sETH and DAI + return items.length === 2; + }, + }, + services: { + fetchBalance: async (ctx) => { + if (!ctx.fuel) { + throw new Error('Fuel Wallet is not detected!'); + } + const [address] = await ctx.fuel.accounts(); + if (!address) { + throw Error('No account found!'); + } + const wallet = await ctx.fuel.getWallet(address); + const balance = await wallet.getBalance(); + return balance; + }, + fetchAssets: async (ctx) => { + if (!ctx.fuel) { + throw new Error('Fuel Wallet is not detected!'); + } + const assetsOnWallet = await ctx.fuel.assets(); + const assetsOnWalletIds = assetsOnWallet.map((a) => a.assetId); + return assetsOnWalletIds; + }, + addAssets: async (ctx) => { + if (!ctx.fuel) { + throw new Error('Fuel Wallet is not detected!'); + } + const assetsOnWallet = await ctx.fuel.assets(); + const assetsOnWalletIds = assetsOnWallet.map((a) => a.assetId); + const assets = SWAYSWAP_ASSETS.filter((a) => !assetsOnWalletIds.includes(a.assetId)); + if (assets.length !== 0) { + await ctx.fuel.addAssets(assets); + } + }, + fetchMintConditions: async (ctx) => { + if (!ctx.fuel) { + throw new Error('Fuel Wallet is not detected!'); + } + const [address] = await ctx.fuel.accounts(); + if (!address) { + throw Error('No account found!'); + } + const wallet = await ctx.fuel.getWallet(address); + const token1 = TokenContractAbi__factory.connect(ETH.assetId, wallet); + const token2 = TokenContractAbi__factory.connect(DAI.assetId, wallet); + const addressId = { + value: wallet.address.toHexString(), + }; + + const { value: hasMint1 } = await token1.functions.has_mint(addressId).get(); + const { value: hasMint2 } = await token2.functions.has_mint(addressId).get(); + + return [hasMint1, hasMint2]; + }, + mintAssets: async (ctx) => { + if (!ctx.fuel) { + throw new Error('Fuel Wallet is not detected!'); + } + const [address] = await ctx.fuel.accounts(); + if (!address) { + throw Error('No account found!'); + } + const wallet = await ctx.fuel.getWallet(address); + const token1 = TokenContractAbi__factory.connect(ETH.assetId, wallet); + const token2 = TokenContractAbi__factory.connect(DAI.assetId, wallet); + const calls = []; + + const addressId = { + value: wallet.address.toHexString(), + }; + const { value: hasMint1 } = await token1.functions.has_mint(addressId).get(); + if (!hasMint1) { + calls.push(token1.functions.mint()); + } + const { value: hasMint2 } = await token2.functions.has_mint(addressId).get(); + if (!hasMint2) { + calls.push(token2.functions.mint()); + } + + if (calls.length === 0) { + return; + } + + await token1.multiCall(calls).txParams(getOverrides()).call(); + }, + }, + } +); + +export type WelcomeMachine = typeof welcomeMachine; +export type WelcomeMachineService = InterpreterFrom; +export type WelcomeMachineState = StateFrom; +export type WelcomeMachineContext = { + state: StateFrom; + send: WelcomeMachineService['send']; + service: WelcomeMachineService; + next: () => void; +}; diff --git a/packages/app/src/systems/Welcome/pages/WelcomePage.tsx b/packages/app/src/systems/Welcome/pages/WelcomePage.tsx index 9f37da71..d30dcb8c 100644 --- a/packages/app/src/systems/Welcome/pages/WelcomePage.tsx +++ b/packages/app/src/systems/Welcome/pages/WelcomePage.tsx @@ -1,43 +1,33 @@ -import { useSelector } from "@xstate/react"; -import { AnimatePresence } from "framer-motion"; -import { Navigate, Outlet, Route, Routes, useLocation } from "react-router-dom"; +import { Outlet, Route, Routes, useLocation } from "react-router-dom"; import { WelcomeSidebar, - WelcomeDone, + WelcomeTerms, StepsIndicator, AddAssets, AddFunds, } from "../components"; import { MintAssets } from "../components/MintAssets"; import { WelcomeConnect } from "../components/WelcomeConnect"; -import { useWelcomeSteps, stepsSelectors } from "../hooks"; import { useBreakpoint } from "~/systems/Core"; import { Pages } from "~/types"; export function WelcomePage() { - const { service } = useWelcomeSteps(); - const isFinished = useSelector(service, stepsSelectors.isFinished); const location = useLocation(); const breakpoint = useBreakpoint(); - if (isFinished) { - return ; - } return (

{breakpoint === "lg" && }
- - - } /> - } /> - } /> - } /> - } /> - - + + } /> + } /> + } /> + } /> + } /> +
diff --git a/packages/app/src/systems/Welcome/routes.tsx b/packages/app/src/systems/Welcome/routes.tsx index e60e3a1b..2513f6a7 100644 --- a/packages/app/src/systems/Welcome/routes.tsx +++ b/packages/app/src/systems/Welcome/routes.tsx @@ -1,6 +1,6 @@ import { Route } from "react-router-dom"; -import { StepsProvider } from "./hooks"; +import { WelcomeStepsProvider } from "./hooks"; import { WelcomePage } from "./pages"; import { Pages } from "~/types"; @@ -9,9 +9,9 @@ export const welcomeRoutes = ( + - + } /> ); diff --git a/packages/app/src/types/contracts/factories/ExchangeContractAbi__factory.ts b/packages/app/src/types/contracts/factories/ExchangeContractAbi__factory.ts index 60724b2d..223d4dcf 100644 --- a/packages/app/src/types/contracts/factories/ExchangeContractAbi__factory.ts +++ b/packages/app/src/types/contracts/factories/ExchangeContractAbi__factory.ts @@ -178,14 +178,14 @@ const _abi = { typeArguments: null, }, attributes: [ - { - name: 'payable', - arguments: [], - }, { name: 'storage', arguments: ['read', 'write'], }, + { + name: 'payable', + arguments: [], + }, ], }, { @@ -399,14 +399,14 @@ const _abi = { typeArguments: null, }, attributes: [ - { - name: 'storage', - arguments: ['read', 'write'], - }, { name: 'payable', arguments: [], }, + { + name: 'storage', + arguments: ['read', 'write'], + }, ], }, { diff --git a/packages/app/src/types/index.ts b/packages/app/src/types/index.ts index 6505c3b9..9112c112 100644 --- a/packages/app/src/types/index.ts +++ b/packages/app/src/types/index.ts @@ -16,11 +16,12 @@ export enum Pages { 'pool.addLiquidity' = 'add-liquidity', 'pool.removeLiquidity' = 'remove-liquidity', 'welcome' = '/welcome', - 'connect' = 'connect', - 'faucet' = 'faucet', - 'addAssets' = 'add-assets', - 'mint' = 'mint', - 'welcome.done' = 'done', + 'welcomeInstall' = 'install', + 'welcomeConnect' = 'connect', + 'welcomeFaucet' = 'faucet', + 'welcomeTerms' = 'terms', + 'welcomeAddAssets' = 'add-assets', + 'welcomeMint' = 'mint', } export enum Queries {