diff --git a/packages/frontend/.env.example b/packages/frontend/.env.example
index 88f6bc1f..75671697 100644
--- a/packages/frontend/.env.example
+++ b/packages/frontend/.env.example
@@ -15,3 +15,4 @@ VITE_PASSKEY_WALLET_RPID=
VITE_TURNKEY_API_BASE_URL=
VITE_LACONICD_CHAIN_ID=
+VITE_IFRAME_ORIGIN_URL=
diff --git a/packages/frontend/src/components/projects/create/AccountsDropdown.tsx b/packages/frontend/src/components/projects/create/AccountsDropdown.tsx
new file mode 100644
index 00000000..e404b093
--- /dev/null
+++ b/packages/frontend/src/components/projects/create/AccountsDropdown.tsx
@@ -0,0 +1,49 @@
+import { Select, Option } from '@snowballtools/material-tailwind-react-fork';
+
+const AccountsDropdown = ({
+ accounts,
+ onAccountChange,
+}: {
+ accounts: string[];
+ onAccountChange: (selectedAccount: string) => void;
+}) => {
+
+ return (
+
+ {!accounts.length ? (
+
+ ) : (
+
+
+
+ )}
+
+ );
+};
+
+export default AccountsDropdown;
diff --git a/packages/frontend/src/components/projects/create/Configure.tsx b/packages/frontend/src/components/projects/create/Configure.tsx
index 6d29d1de..a8731ba5 100644
--- a/packages/frontend/src/components/projects/create/Configure.tsx
+++ b/packages/frontend/src/components/projects/create/Configure.tsx
@@ -20,10 +20,11 @@ import { Button } from '../../shared/Button';
import { Input } from 'components/shared/Input';
import { useToast } from 'components/shared/Toast';
import { useGQLClient } from '../../../context/GQLClientContext';
-import IFrame from './IFrame';
+import IFrameModal from './IFrameModal';
import EnvironmentVariablesForm from 'pages/org-slug/projects/id/settings/EnvironmentVariablesForm';
import { EnvironmentVariablesFormValues } from 'types/types';
-import { VITE_LACONICD_CHAIN_ID } from 'utils/constants';
+import { VITE_LACONICD_CHAIN_ID, VITE_IFRAME_ORIGIN_URL } from 'utils/constants';
+import AccountsDropdown from './AccountsDropdown';
type ConfigureDeploymentFormValues = {
option: string;
@@ -36,6 +37,7 @@ type ConfigureFormValues = ConfigureDeploymentFormValues &
EnvironmentVariablesFormValues;
const DEFAULT_MAX_PRICE = '10000';
+const TX_APPROVAL_TIMEOUT = 60000;
const Configure = () => {
const [isLoading, setIsLoading] = useState(false);
@@ -175,7 +177,6 @@ const Configure = () => {
const handleFormSubmit = useCallback(
async (createFormData: FieldValues) => {
try {
- setIsFrameVisible(true);
const deployerLrn = createFormData.lrn;
const deployer = deployers.find(
(deployer) => deployer.deployerLrn === deployerLrn,
@@ -213,6 +214,33 @@ const Configure = () => {
senderAddress,
amountToBePaid,
);
+
+ if (!txHash) {
+ return;
+ }
+
+ // Validate transaction hash
+ const isTxHashValid = await verifyTx(
+ senderAddress,
+ txHash,
+ amount,
+ );
+
+ if (isTxHashValid) {
+ toast({
+ id: 'payment-successful',
+ title: 'Payment successful',
+ variant: 'success',
+ onDismiss: dismiss,
+ });
+ } else {
+ toast({
+ id: 'invalid-tx-hash',
+ title: 'Transaction validation failed',
+ variant: 'error',
+ onDismiss: dismiss,
+ });
+ }
}
const environmentVariables = createFormData.variables.map(
@@ -300,48 +328,16 @@ const Configure = () => {
setIsPaymentDone(false);
setIsPaymentLoading(true);
- toast({
- id: 'sending-payment-request',
- title: 'Check and approve payment request',
- variant: 'loading',
- onDismiss: dismiss,
- });
-
await requestTx(senderAddress, snowballAddress, amount);
const txHash = await new Promise((resolve, reject) => {
const handleTxStatus = async (event: MessageEvent) => {
- if (event.origin !== 'http://localhost:3001') return;
+ if (event.origin !== VITE_IFRAME_ORIGIN_URL) return;
if (event.data.type === 'TRANSACTION_SUCCESS') {
const txResponse = event.data.data;
-
+ resolve(txResponse);
setIsFrameVisible(false);
-
- // Validate transaction hash
- const isTxHashValid = await verifyTx(
- senderAddress,
- txResponse,
- amount,
- );
-
- if (isTxHashValid) {
- resolve(txResponse);
- toast({
- id: 'payment-successful',
- title: 'Payment successful',
- variant: 'success',
- onDismiss: dismiss,
- });
- } else {
- reject(new Error('Invalid transaction hash'));
- toast({
- id: 'invalid-tx-hash',
- title: 'Transaction validation failed',
- variant: 'error',
- onDismiss: dismiss,
- });
- }
} else if (event.data.type === 'ERROR') {
console.error('Error from wallet:', event.data.message);
reject(new Error('Transaction failed'));
@@ -362,7 +358,13 @@ const Configure = () => {
setTimeout(() => {
reject(new Error('Transaction timeout'));
window.removeEventListener('message', handleTxStatus);
- }, 60000);
+ toast({
+ id: 'transaction-timeout',
+ title: 'The transaction request timed out. Please try again',
+ variant: 'error',
+ onDismiss: dismiss,
+ });
+ }, TX_APPROVAL_TIMEOUT);
});
return txHash;
} catch (error) {
@@ -391,8 +393,10 @@ const Configure = () => {
toAddress: recipient,
amount,
},
- 'http://localhost:3001'
+ VITE_IFRAME_ORIGIN_URL
);
+
+ setIsFrameVisible(true);
};
useEffect(() => {
@@ -561,10 +565,9 @@ const Configure = () => {
) : (
<>
-
+
diff --git a/packages/frontend/src/components/projects/create/IFrame.tsx b/packages/frontend/src/components/projects/create/IFrame.tsx
deleted file mode 100644
index e31d8baa..00000000
--- a/packages/frontend/src/components/projects/create/IFrame.tsx
+++ /dev/null
@@ -1,124 +0,0 @@
-import { useCallback, useEffect } from 'react';
-
-import { Select, Option } from '@snowballtools/material-tailwind-react-fork';
-import { Box, Modal } from '@mui/material';
-
-import { VITE_LACONICD_CHAIN_ID } from 'utils/constants';
-
-const IFrame = ({
- accounts,
- setAccounts,
- onAccountChange,
- isVisible,
- toggleModal,
-}: {
- accounts: string[];
- setAccounts: (accounts: string[]) => void;
- onAccountChange: (selectedAccount: string) => void;
- isVisible: boolean;
- toggleModal: () => void;
-}) => {
- useEffect(() => {
- const handleMessage = (event: MessageEvent) => {
- // TODO: Use env for origin URL
- if (event.origin !== 'http://localhost:3001') return;
-
- if (event.data.type === 'WALLET_ACCOUNTS_DATA') {
- setAccounts(event.data.data);
- } else if (event.data.type === 'ERROR') {
- console.error('Error from wallet:', event.data.message);
- }
- };
-
- window.addEventListener('message', handleMessage);
-
- return () => {
- window.removeEventListener('message', handleMessage);
- };
- }, []);
-
- const getDataFromWallet = useCallback(() => {
- const iframe = document.getElementById('walletIframe') as HTMLIFrameElement;
-
- if (!iframe.contentWindow) {
- console.error('Iframe not found or not loaded');
- return;
- }
-
- iframe.contentWindow.postMessage(
- {
- type: 'REQUEST_WALLET_ACCOUNTS',
- chainId: VITE_LACONICD_CHAIN_ID,
- },
- 'http://localhost:3001'
- );
- }, []);
-
- return (
-
- {!accounts.length ? (
-
- ) : (
-
-
-
- )}
-
-
-
-
-
-
- );
-};
-
-export default IFrame;
diff --git a/packages/frontend/src/components/projects/create/IFrameModal.tsx b/packages/frontend/src/components/projects/create/IFrameModal.tsx
new file mode 100644
index 00000000..b7db68e5
--- /dev/null
+++ b/packages/frontend/src/components/projects/create/IFrameModal.tsx
@@ -0,0 +1,85 @@
+import { useCallback, useEffect } from 'react';
+
+import { Box, Modal } from '@mui/material';
+
+import { VITE_LACONICD_CHAIN_ID, VITE_IFRAME_ORIGIN_URL } from 'utils/constants';
+
+const IFrameModal = ({
+ setAccounts,
+ isVisible,
+ toggleModal,
+}: {
+ setAccounts: (accounts: string[]) => void;
+ isVisible: boolean;
+ toggleModal: () => void;
+}) => {
+ useEffect(() => {
+ const handleMessage = (event: MessageEvent) => {
+ // TODO: Use env for origin URL
+ if (event.origin !== VITE_IFRAME_ORIGIN_URL) return;
+
+ if (event.data.type === 'WALLET_ACCOUNTS_DATA') {
+ setAccounts(event.data.data);
+ } else if (event.data.type === 'ERROR') {
+ console.error('Error from wallet:', event.data.message);
+ }
+ };
+
+ window.addEventListener('message', handleMessage);
+
+ return () => {
+ window.removeEventListener('message', handleMessage);
+ };
+ }, []);
+
+ const getDataFromWallet = useCallback(() => {
+ const iframe = document.getElementById('walletIframe') as HTMLIFrameElement;
+
+ if (!iframe.contentWindow) {
+ console.error('Iframe not found or not loaded');
+ return;
+ }
+
+ iframe.contentWindow.postMessage(
+ {
+ type: 'REQUEST_WALLET_ACCOUNTS',
+ chainId: VITE_LACONICD_CHAIN_ID,
+ },
+ VITE_IFRAME_ORIGIN_URL
+ );
+ }, []);
+
+ return (
+
+
+
+
+
+ )
+};
+
+export default IFrameModal;
diff --git a/packages/frontend/src/context/WalletConnectContext.tsx b/packages/frontend/src/context/WalletConnectContext.tsx
deleted file mode 100644
index e5dc4fc9..00000000
--- a/packages/frontend/src/context/WalletConnectContext.tsx
+++ /dev/null
@@ -1,210 +0,0 @@
-import {
- createContext,
- useCallback,
- useContext,
- useEffect,
- useRef,
- useState,
-} from 'react';
-
-import SignClient from '@walletconnect/sign-client';
-import { getSdkError } from '@walletconnect/utils';
-import { SessionTypes } from '@walletconnect/types';
-
-import { walletConnectModal } from '../utils/web3modal';
-import {
- VITE_LACONICD_CHAIN_ID,
- VITE_WALLET_CONNECT_ID,
-} from 'utils/constants';
-
-interface ClientInterface {
- signClient: SignClient | undefined;
- session: SessionTypes.Struct | undefined;
- loadingSession: boolean;
- onConnect: () => Promise;
- onDisconnect: () => Promise;
- onSessionDelete: () => void;
- accounts: { address: string }[];
-}
-
-const ClientContext = createContext({} as ClientInterface);
-
-export const useWalletConnectClient = () => {
- return useContext(ClientContext);
-};
-
-export const WalletConnectClientProvider = ({
- children,
-}: {
- children: JSX.Element;
-}) => {
- const [signClient, setSignClient] = useState();
- const [session, setSession] = useState();
- const [loadingSession, setLoadingSession] = useState(true);
- const [accounts, setAccounts] = useState<{ address: string }[]>([]);
-
- const isSignClientInitializing = useRef(false);
-
- const onSessionConnect = useCallback(async (session: SessionTypes.Struct) => {
- setSession(session);
- }, []);
-
- const subscribeToEvents = useCallback(
- async (client: SignClient) => {
- client.on('session_update', ({ topic, params }) => {
- const { namespaces } = params;
- const currentSession = client.session.get(topic);
- const updatedSession = { ...currentSession, namespaces };
- setSession(updatedSession);
- });
- },
- [setSession],
- );
-
- const onConnect = async () => {
- const proposalNamespace = {
- cosmos: {
- methods: ['cosmos_sendTokens'],
- chains: [`cosmos:${VITE_LACONICD_CHAIN_ID}`],
- events: [],
- },
- };
-
- try {
- const { uri, approval } = await signClient!.connect({
- requiredNamespaces: proposalNamespace,
- });
-
- if (uri) {
- walletConnectModal.openModal({ uri });
- const session = await approval();
- onSessionConnect(session);
- walletConnectModal.closeModal();
- }
- } catch (e) {
- console.error(e);
- }
- };
-
- const onDisconnect = useCallback(async () => {
- if (typeof signClient === 'undefined') {
- throw new Error('WalletConnect is not initialized');
- }
- if (typeof session === 'undefined') {
- throw new Error('Session is not connected');
- }
-
- await signClient.disconnect({
- topic: session.topic,
- reason: getSdkError('USER_DISCONNECTED'),
- });
-
- onSessionDelete();
- }, [signClient, session]);
-
- const onSessionDelete = () => {
- setAccounts([]);
- setSession(undefined);
- };
-
- const checkPersistedState = useCallback(
- async (signClient: SignClient) => {
- if (typeof signClient === 'undefined') {
- throw new Error('WalletConnect is not initialized');
- }
-
- if (typeof session !== 'undefined') return;
- if (signClient.session.length) {
- const lastKeyIndex = signClient.session.keys.length - 1;
- const previousSsession = signClient.session.get(
- signClient.session.keys[lastKeyIndex],
- );
-
- await onSessionConnect(previousSsession);
- return previousSsession;
- }
- },
- [session, onSessionConnect],
- );
-
- const createClient = useCallback(async () => {
- isSignClientInitializing.current = true;
- try {
- const signClient = await SignClient.init({
- projectId: VITE_WALLET_CONNECT_ID,
- metadata: {
- name: 'Deploy App',
- description: '',
- url: window.location.href,
- icons: ['https://avatars.githubusercontent.com/u/92608123'],
- },
- });
-
- setSignClient(signClient);
- await checkPersistedState(signClient);
- await subscribeToEvents(signClient);
- setLoadingSession(false);
- } catch (e) {
- console.error('error in createClient', e);
- }
- isSignClientInitializing.current = false;
- }, [setSignClient, checkPersistedState, subscribeToEvents]);
-
- useEffect(() => {
- if (!signClient && !isSignClientInitializing.current) {
- createClient();
- }
- }, [signClient, createClient]);
-
- useEffect(() => {
- const populateAccounts = async () => {
- if (!session) {
- return;
- }
- if (!session.namespaces['cosmos']) {
- console.log('Accounts for cosmos namespace not found');
- return;
- }
-
- const cosmosAddresses = session.namespaces['cosmos'].accounts;
-
- const cosmosAccounts = cosmosAddresses.map((address) => ({
- address,
- }));
-
- const allAccounts = cosmosAccounts;
-
- setAccounts(allAccounts);
- };
-
- populateAccounts();
- }, [session]);
-
- useEffect(() => {
- if (!signClient) {
- return;
- }
-
- signClient.on('session_delete', onSessionDelete);
-
- return () => {
- signClient.off('session_delete', onSessionDelete);
- };
- });
-
- return (
-
- {children}
-
- );
-};
diff --git a/packages/frontend/src/index.tsx b/packages/frontend/src/index.tsx
index 787cef61..862eaf67 100644
--- a/packages/frontend/src/index.tsx
+++ b/packages/frontend/src/index.tsx
@@ -4,10 +4,9 @@ import assert from 'assert';
import { GQLClient } from 'gql-client';
import { ThemeProvider } from '@snowballtools/material-tailwind-react-fork';
-
-import './index.css';
import '@fontsource/inter';
import '@fontsource-variable/jetbrains-mono';
+
import App from './App';
import reportWebVitals from './reportWebVitals';
import { GQLClientProvider } from './context/GQLClientContext';
@@ -16,7 +15,7 @@ import { Toaster } from 'components/shared/Toast';
import { LogErrorBoundary } from 'utils/log-error';
import { BASE_URL } from 'utils/constants';
import Web3ModalProvider from './context/Web3Provider';
-import { WalletConnectClientProvider } from 'context/WalletConnectContext';
+import './index.css';
console.log(`v-0.0.9`);
@@ -32,16 +31,14 @@ const gqlClient = new GQLClient({ gqlEndpoint });
root.render(
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
,
);
diff --git a/packages/frontend/src/utils/constants.ts b/packages/frontend/src/utils/constants.ts
index 446d9d58..5a0d23ff 100644
--- a/packages/frontend/src/utils/constants.ts
+++ b/packages/frontend/src/utils/constants.ts
@@ -12,3 +12,4 @@ export const VITE_WALLET_CONNECT_ID = import.meta.env.VITE_WALLET_CONNECT_ID;
export const VITE_BUGSNAG_API_KEY = import.meta.env.VITE_BUGSNAG_API_KEY;
export const VITE_LIT_RELAY_API_KEY = import.meta.env.VITE_LIT_RELAY_API_KEY;
export const VITE_LACONICD_CHAIN_ID = import.meta.env.VITE_LACONICD_CHAIN_ID;
+export const VITE_IFRAME_ORIGIN_URL = import.meta.env.VITE_IFRAME_ORIGIN_URL;