diff --git a/electron/install.js b/electron/install.js index ea3c949f5..833977791 100644 --- a/electron/install.js +++ b/electron/install.js @@ -14,7 +14,7 @@ const homedir = os.homedir(); * - use "" (nothing as a suffix) for latest release candidate, for example "0.1.0rc26" * - use "alpha" for alpha release, for example "0.1.0rc26-alpha" */ -const OlasMiddlewareVersion = '0.1.0rc158'; +const OlasMiddlewareVersion = '0.1.0rc162'; const path = require('path'); const { app } = require('electron'); diff --git a/frontend/components/SetupPage/Create/SetupCreateSafe.tsx b/frontend/components/SetupPage/Create/SetupCreateSafe.tsx index dc4ab9a9e..0f4813879 100644 --- a/frontend/components/SetupPage/Create/SetupCreateSafe.tsx +++ b/frontend/components/SetupPage/Create/SetupCreateSafe.tsx @@ -1,40 +1,100 @@ import { message, Typography } from 'antd'; import Image from 'next/image'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { Chain } from '@/client'; import { CardSection } from '@/components/styled/CardSection'; import { UNICODE_SYMBOLS } from '@/constants/symbols'; import { SUPPORT_URL } from '@/constants/urls'; import { Pages } from '@/enums/PageState'; +import { useMasterSafe } from '@/hooks/useMasterSafe'; import { usePageState } from '@/hooks/usePageState'; import { useSetup } from '@/hooks/useSetup'; import { useWallet } from '@/hooks/useWallet'; import { WalletService } from '@/service/Wallet'; +import { delayInSeconds } from '@/utils/delay'; export const SetupCreateSafe = () => { - const { masterSafeAddress } = useWallet(); - const { backupSigner } = useSetup(); const { goto } = usePageState(); + const { updateWallets } = useWallet(); + const { updateMasterSafeOwners, masterSafeAddress, backupSafeAddress } = + useMasterSafe(); + const { backupSigner } = useSetup(); const [isCreatingSafe, setIsCreatingSafe] = useState(false); - const [isError, setIsError] = useState(false); + const [isCreateSafeSuccessful, setIsCreateSafeSuccessful] = useState(false); + const [failed, setFailed] = useState(false); + + const createSafeWithRetries = useCallback( + async (retries: number) => { + setIsCreatingSafe(true); + + // If we have retried too many times, set failed + if (retries <= 0) { + setFailed(true); + setIsCreatingSafe(false); + setIsCreateSafeSuccessful(false); + return; + } + + // Try to create the safe + WalletService.createSafe(Chain.GNOSIS, backupSigner) + .then(async () => { + // Backend returned success + message.success('Account created'); + + // Attempt wallet and master safe updates before proceeding + try { + await updateWallets(); + await updateMasterSafeOwners(); + } catch (e) { + console.error(e); + } + + // Set states for successful creation + setIsCreatingSafe(false); + setIsCreateSafeSuccessful(true); + setFailed(false); + }) + .catch(async (e) => { + console.error(e); + // Wait for 5 seconds before retrying + await delayInSeconds(5); + // Retry + const newRetries = retries - 1; + if (newRetries <= 0) { + message.error('Failed to create account'); + } else { + message.error('Failed to create account, retrying in 5 seconds'); + } + createSafeWithRetries(newRetries); + }); + }, + [backupSigner, updateMasterSafeOwners, updateWallets], + ); + + const creationStatusText = useMemo(() => { + if (isCreatingSafe) return 'Creating account'; + if (masterSafeAddress && !backupSafeAddress) return 'Checking backup'; + if (masterSafeAddress && backupSafeAddress) return 'Account created'; + return 'Account creation in progress'; + }, [backupSafeAddress, isCreatingSafe, masterSafeAddress]); useEffect(() => { - if (isCreatingSafe) return; - setIsCreatingSafe(true); - // TODO: add backup signer - WalletService.createSafe(Chain.GNOSIS, backupSigner).catch((e) => { - console.error(e); - setIsError(true); - message.error('Failed to create an account. Please try again later.'); - }); - }, [backupSigner, isCreatingSafe]); + if (failed || isCreatingSafe || isCreateSafeSuccessful) return; + createSafeWithRetries(3); + }, [ + backupSigner, + createSafeWithRetries, + failed, + isCreateSafeSuccessful, + isCreatingSafe, + ]); useEffect(() => { // Only progress is the safe is created and accessible via context (updates on interval) - if (masterSafeAddress) goto(Pages.Main); - }, [goto, masterSafeAddress]); + if (masterSafeAddress && backupSafeAddress) goto(Pages.Main); + }, [backupSafeAddress, goto, masterSafeAddress]); return ( { padding="80px 24px" gap={12} > - {isError ? ( + {failed ? ( <> logo - Error, please contact{' '} - - Olas community {UNICODE_SYMBOLS.EXTERNAL_LINK} + Error, please restart the app and try again. + + + If the issue persists, please{' '} + + contact Olas community support {UNICODE_SYMBOLS.EXTERNAL_LINK} + . ) : ( @@ -67,7 +131,7 @@ export const SetupCreateSafe = () => { className="m-0 mt-12 loading-ellipses" style={{ width: '220px' }} > - Creating account + {creationStatusText} You will be redirected once the account is created diff --git a/frontend/components/SetupPage/Create/SetupSeedPhrase.tsx b/frontend/components/SetupPage/Create/SetupSeedPhrase.tsx index 5b32a4cdb..14fbac876 100644 --- a/frontend/components/SetupPage/Create/SetupSeedPhrase.tsx +++ b/frontend/components/SetupPage/Create/SetupSeedPhrase.tsx @@ -28,19 +28,21 @@ export const SetupSeedPhrase = () => { {word} ))} - - + + + + ); }; diff --git a/frontend/components/styled/CardFlex.tsx b/frontend/components/styled/CardFlex.tsx index 5ad9a5b6f..965ff9d19 100644 --- a/frontend/components/styled/CardFlex.tsx +++ b/frontend/components/styled/CardFlex.tsx @@ -13,7 +13,7 @@ export const CardFlex = styled(Card).withConfig({ const { gap, noBodyPadding } = props; const gapStyle = gap ? `gap: ${gap}px;` : ''; - const paddingStyle = noBodyPadding === 'true' ? 'padding: 0;' : undefined; + const paddingStyle = noBodyPadding === 'true' ? 'padding: 0;' : ''; return `${gapStyle} ${paddingStyle}`; }} diff --git a/frontend/constants/headers.ts b/frontend/constants/headers.ts new file mode 100644 index 000000000..aedf09f9c --- /dev/null +++ b/frontend/constants/headers.ts @@ -0,0 +1,3 @@ +export const CONTENT_TYPE_JSON_UTF8 = { + 'Content-Type': 'application/json; charset=utf-8', +} as const; diff --git a/frontend/context/MasterSafeProvider.tsx b/frontend/context/MasterSafeProvider.tsx index 46daeaba1..fb654eae6 100644 --- a/frontend/context/MasterSafeProvider.tsx +++ b/frontend/context/MasterSafeProvider.tsx @@ -19,7 +19,7 @@ export const MasterSafeContext = createContext<{ masterSafeAddress?: Address; masterEoaAddress?: Address; masterSafeOwners?: Address[]; - updateMasterSafeOwners?: () => Promise; + updateMasterSafeOwners: () => Promise; }>({ backupSafeAddress: undefined, masterSafeAddress: undefined, diff --git a/frontend/context/ServicesProvider.tsx b/frontend/context/ServicesProvider.tsx index a96b43635..39a9a5e0b 100644 --- a/frontend/context/ServicesProvider.tsx +++ b/frontend/context/ServicesProvider.tsx @@ -84,7 +84,8 @@ export const ServicesProvider = ({ children }: PropsWithChildren) => { setHasInitialLoaded(true); }) .catch((e) => { - message.error(e.message); + console.error(e); + // message.error(e.message); Commented out to avoid showing error message; need to handle "isAuthenticated" in a better way }), [], ); diff --git a/frontend/context/WalletProvider.tsx b/frontend/context/WalletProvider.tsx index 21c02606a..bbf27c183 100644 --- a/frontend/context/WalletProvider.tsx +++ b/frontend/context/WalletProvider.tsx @@ -29,9 +29,12 @@ export const WalletProvider = ({ children }: PropsWithChildren) => { const masterSafeAddress: Address | undefined = wallets?.[0]?.safe; const updateWallets = async () => { - const wallets = await WalletService.getWallets(); - if (!wallets) return; - setWallets(wallets); + try { + const wallets = await WalletService.getWallets(); + setWallets(wallets); + } catch (e) { + console.error(e); + } }; useInterval(updateWallets, isOnline ? FIVE_SECONDS_INTERVAL : null); diff --git a/frontend/service/Account.ts b/frontend/service/Account.ts index 49a61c29a..acb638d72 100644 --- a/frontend/service/Account.ts +++ b/frontend/service/Account.ts @@ -1,10 +1,18 @@ +import { CONTENT_TYPE_JSON_UTF8 } from '@/constants/headers'; import { BACKEND_URL } from '@/constants/urls'; /** * Gets account status "is_setup" */ const getAccount = () => - fetch(`${BACKEND_URL}/account`).then((res) => res.json()); + fetch(`${BACKEND_URL}/account`, { + headers: { + ...CONTENT_TYPE_JSON_UTF8, + }, + }).then((res) => { + if (res.ok) return res.json(); + throw new Error('Failed to get account'); + }); /** * Creates a local user account @@ -13,10 +21,13 @@ const createAccount = (password: string) => fetch(`${BACKEND_URL}/account`, { method: 'POST', headers: { - 'Content-Type': 'application/json', + ...CONTENT_TYPE_JSON_UTF8, }, body: JSON.stringify({ password }), - }).then((res) => res.json()); + }).then((res) => { + if (res.ok) return res.json(); + throw new Error('Failed to create account'); + }); /** * Updates user's password @@ -24,14 +35,15 @@ const createAccount = (password: string) => const updateAccount = (oldPassword: string, newPassword: string) => fetch(`${BACKEND_URL}/account`, { method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, + headers: { ...CONTENT_TYPE_JSON_UTF8 }, body: JSON.stringify({ old_password: oldPassword, new_password: newPassword, }), - }).then((res) => res.json()); + }).then((res) => { + if (res.ok) return res.json(); + throw new Error('Failed to update account'); + }); /** * Logs in a user @@ -39,15 +51,13 @@ const updateAccount = (oldPassword: string, newPassword: string) => const loginAccount = (password: string) => fetch(`${BACKEND_URL}/account/login`, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + headers: { ...CONTENT_TYPE_JSON_UTF8 }, body: JSON.stringify({ password, }), }).then((res) => { - if (![200, 201].includes(res.status)) throw new Error('Login failed'); - res.json(); + if (res.ok) return res.json(); + throw new Error('Failed to login'); }); export const AccountService = { diff --git a/frontend/service/Services.ts b/frontend/service/Services.ts index be0a2c637..a50959747 100644 --- a/frontend/service/Services.ts +++ b/frontend/service/Services.ts @@ -1,4 +1,5 @@ import { Deployment, Service, ServiceHash, ServiceTemplate } from '@/client'; +import { CONTENT_TYPE_JSON_UTF8 } from '@/constants/headers'; import { BACKEND_URL } from '@/constants/urls'; import { StakingProgramId } from '@/enums/StakingProgram'; @@ -8,9 +9,17 @@ import { StakingProgramId } from '@/enums/StakingProgram'; * @returns */ const getService = async (serviceHash: ServiceHash): Promise => - fetch(`${BACKEND_URL}/services/${serviceHash}`).then((response) => - response.json(), - ); + fetch(`${BACKEND_URL}/services/${serviceHash}`, { + method: 'GET', + headers: { + ...CONTENT_TYPE_JSON_UTF8, + }, + }).then((response) => { + if (response.ok) { + return response.json(); + } + throw new Error(`Failed to fetch service ${serviceHash}`); + }); /** * Gets an array of services from the backend @@ -20,9 +29,14 @@ const getServices = async (): Promise => fetch(`${BACKEND_URL}/services`, { method: 'GET', headers: { - 'Content-Type': 'application/json', + ...CONTENT_TYPE_JSON_UTF8, }, - }).then((response) => response.json()); + }).then((response) => { + if (response.ok) { + return response.json(); + } + throw new Error('Failed to fetch services'); + }); /** * Creates a service @@ -53,7 +67,7 @@ const createService = async ({ }, }), headers: { - 'Content-Type': 'application/json', + ...CONTENT_TYPE_JSON_UTF8, }, }).then((response) => { if (response.ok) { @@ -66,39 +80,95 @@ const createService = async ({ const deployOnChain = async (serviceHash: ServiceHash): Promise => fetch(`${BACKEND_URL}/services/${serviceHash}/onchain/deploy`, { method: 'POST', - }).then((response) => response.json()); + headers: { + ...CONTENT_TYPE_JSON_UTF8, + }, + }).then((response) => { + if (response.ok) { + return response.json(); + } + throw new Error('Failed to deploy service on chain'); + }); const stopOnChain = async (serviceHash: ServiceHash): Promise => fetch(`${BACKEND_URL}/services/${serviceHash}/onchain/stop`, { method: 'POST', - }).then((response) => response.json()); + headers: { + ...CONTENT_TYPE_JSON_UTF8, + }, + }).then((response) => { + if (response.ok) { + return response.json(); + } + throw new Error('Failed to stop service on chain'); + }); const buildDeployment = async (serviceHash: ServiceHash): Promise => fetch(`${BACKEND_URL}/services/${serviceHash}/deployment/build`, { method: 'POST', - }).then((response) => response.json()); + headers: { + ...CONTENT_TYPE_JSON_UTF8, + }, + }).then((response) => { + if (response.ok) { + return response.json(); + } + throw new Error('Failed to build deployment'); + }); const startDeployment = async (serviceHash: ServiceHash): Promise => fetch(`${BACKEND_URL}/services/${serviceHash}/deployment/start`, { method: 'POST', - }).then((response) => response.json()); + headers: { + ...CONTENT_TYPE_JSON_UTF8, + }, + }).then((response) => { + if (response.ok) { + return response.json(); + } + throw new Error('Failed to start deployment'); + }); const stopDeployment = async (serviceHash: ServiceHash): Promise => fetch(`${BACKEND_URL}/services/${serviceHash}/deployment/stop`, { method: 'POST', - }).then((response) => response.json()); + headers: { + ...CONTENT_TYPE_JSON_UTF8, + }, + }).then((response) => { + if (response.ok) { + return response.json(); + } + throw new Error('Failed to stop deployment'); + }); const deleteDeployment = async ( serviceHash: ServiceHash, ): Promise => fetch(`${BACKEND_URL}/services/${serviceHash}/deployment/delete`, { method: 'POST', - }).then((response) => response.json()); + headers: { + ...CONTENT_TYPE_JSON_UTF8, + }, + }).then((response) => { + if (response.ok) { + return response.json(); + } + throw new Error('Failed to delete deployment'); + }); const getDeployment = async (serviceHash: ServiceHash): Promise => - fetch(`${BACKEND_URL}/services/${serviceHash}/deployment`).then((response) => - response.json(), - ); + fetch(`${BACKEND_URL}/services/${serviceHash}/deployment`, { + method: 'GET', + headers: { + ...CONTENT_TYPE_JSON_UTF8, + }, + }).then((response) => { + if (response.ok) { + return response.json(); + } + throw new Error('Failed to get deployment'); + }); export const ServicesService = { getService, diff --git a/frontend/service/Wallet.ts b/frontend/service/Wallet.ts index b09a8e95c..a8909c305 100644 --- a/frontend/service/Wallet.ts +++ b/frontend/service/Wallet.ts @@ -1,38 +1,51 @@ import { Chain } from '@/client'; +import { CONTENT_TYPE_JSON_UTF8 } from '@/constants/headers'; import { BACKEND_URL } from '@/constants/urls'; /** * Returns a list of available wallets */ const getWallets = async () => - fetch(`${BACKEND_URL}/wallet`).then((res) => res.json()); + fetch(`${BACKEND_URL}/wallet`).then((res) => { + if (res.ok) return res.json(); + throw new Error('Failed to get wallets'); + }); const createEoa = async (chain: Chain) => fetch(`${BACKEND_URL}/wallet`, { method: 'POST', headers: { - 'Content-Type': 'application/json', + ...CONTENT_TYPE_JSON_UTF8, }, body: JSON.stringify({ chain_type: chain }), - }).then((res) => res.json()); + }).then((res) => { + if (res.ok) return res.json(); + throw new Error('Failed to create EOA'); + }); const createSafe = async (chain: Chain, owner?: string) => fetch(`${BACKEND_URL}/wallet/safe`, { method: 'POST', headers: { - 'Content-Type': 'application/json', + ...CONTENT_TYPE_JSON_UTF8, }, body: JSON.stringify({ chain_type: chain, owner: owner }), - }).then((res) => res.json()); + }).then((res) => { + if (res.ok) return res.json(); + throw new Error('Failed to create safe'); + }); const addBackupOwner = async (chain: Chain, owner: string) => fetch(`${BACKEND_URL}/wallet/safe`, { method: 'PUT', headers: { - 'Content-Type': 'application/json', + ...CONTENT_TYPE_JSON_UTF8, }, body: JSON.stringify({ chain_type: chain, owner: owner }), - }).then((res) => res.json()); + }).then((res) => { + if (res.ok) return res.json(); + throw new Error('Failed to add backup owner'); + }); export const WalletService = { getWallets, diff --git a/package.json b/package.json index b451c1b31..ebee9fed2 100644 --- a/package.json +++ b/package.json @@ -62,5 +62,5 @@ "download-binaries": "sh download_binaries.sh", "build:pearl": "sh build_pearl.sh" }, - "version": "0.1.0-rc158" + "version": "0.1.0-rc162" } diff --git a/pyproject.toml b/pyproject.toml index 9b664cdf2..6ec3dd9e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "olas-operate-middleware" -version = "0.1.0-rc158" +version = "0.1.0-rc162" description = "" authors = ["David Vilela ", "Viraj Patel "] readme = "README.md"