Integrate wallet IFrame for payments #42
@ -15,3 +15,4 @@ VITE_PASSKEY_WALLET_RPID=
|
||||
VITE_TURNKEY_API_BASE_URL=
|
||||
|
||||
VITE_LACONICD_CHAIN_ID=
|
||||
VITE_IFRAME_ORIGIN_URL=
|
||||
|
@ -0,0 +1,49 @@
|
||||
import { Select, Option } from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
const AccountsDropdown = ({
|
||||
accounts,
|
||||
onAccountChange,
|
||||
}: {
|
||||
accounts: string[];
|
||||
onAccountChange: (selectedAccount: string) => void;
|
||||
}) => {
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-slate-100 dark:bg-overlay3 rounded-lg mb-6 shadow-md">
|
||||
{!accounts.length ? (
|
||||
<div className="text-center">
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
No accounts found. Please visit{' '}
|
||||
<a
|
||||
href="https://store.laconic.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 underline dark:text-blue-400"
|
||||
>
|
||||
store.laconic.com
|
||||
</a>{' '}
|
||||
to create a wallet.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Select
|
||||
label="Select Account"
|
||||
defaultValue={accounts[0]}
|
||||
onChange={(value) => value && onAccountChange(value)}
|
||||
className="dark:bg-overlay2 dark:text-foreground"
|
||||
aria-label="Wallet Account Selector"
|
||||
>
|
||||
{accounts.map((account, index) => (
|
||||
<Option key={index} value={account}>
|
||||
{account}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountsDropdown;
|
@ -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<string>((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;
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
setIsFrameVisible(false);
|
||||
} 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 = () => {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<IFrame
|
||||
accounts={accounts}
|
||||
<AccountsDropdown accounts={accounts} onAccountChange={onAccountChange} />
|
||||
<IFrameModal
|
||||
setAccounts={setAccounts}
|
||||
onAccountChange={onAccountChange}
|
||||
isVisible={isFrameVisible}
|
||||
toggleModal={toggleModal}
|
||||
/>
|
||||
|
@ -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 (
|
||||
<div className="p-6 bg-slate-100 dark:bg-overlay3 rounded-lg mb-6 shadow-md">
|
||||
{!accounts.length ? (
|
||||
<div className="text-center">
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
No accounts found. Please visit{' '}
|
||||
<a
|
||||
href="https://store.laconic.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 underline dark:text-blue-400"
|
||||
>
|
||||
store.laconic.com
|
||||
</a>{' '}
|
||||
to create a wallet.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Select
|
||||
label="Select Account"
|
||||
defaultValue={accounts[0]}
|
||||
onChange={(value) => value && onAccountChange(value)}
|
||||
className="dark:bg-overlay2 dark:text-foreground"
|
||||
aria-label="Wallet Account Selector"
|
||||
>
|
||||
{accounts.map((account, index) => (
|
||||
<Option key={index} value={account}>
|
||||
{account}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
<Modal open={isVisible} onClose={toggleModal} keepMounted>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: '90%',
|
||||
maxWidth: '1200px',
|
||||
height: '600px',
|
||||
maxHeight: '80vh',
|
||||
overflow: 'auto',
|
||||
boxShadow: 24,
|
||||
borderRadius: '8px',
|
||||
outline: 'none',
|
||||
bgcolor: 'background.paper',
|
||||
}}
|
||||
>
|
||||
<iframe
|
||||
onLoad={getDataFromWallet}
|
||||
id="walletIframe"
|
||||
src="http://localhost:3001/WalletEmbed"
|
||||
width="100%"
|
||||
height="100%"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
className="border rounded-md shadow-sm"
|
||||
></iframe>
|
||||
</Box>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IFrame;
|
@ -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 (
|
||||
<Modal open={isVisible} onClose={toggleModal} keepMounted>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: '90%',
|
||||
maxWidth: '1200px',
|
||||
height: '600px',
|
||||
maxHeight: '80vh',
|
||||
overflow: 'auto',
|
||||
boxShadow: 24,
|
||||
borderRadius: '8px',
|
||||
outline: 'none',
|
||||
bgcolor: 'background.paper',
|
||||
}}
|
||||
>
|
||||
<iframe
|
||||
onLoad={getDataFromWallet}
|
||||
id="walletIframe"
|
||||
src={`${VITE_IFRAME_ORIGIN_URL}/WalletEmbed`}
|
||||
width="100%"
|
||||
height="100%"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
className="border rounded-md shadow-sm"
|
||||
></iframe>
|
||||
</Box>
|
||||
</Modal>
|
||||
)
|
||||
};
|
||||
|
||||
export default IFrameModal;
|
@ -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<void>;
|
||||
onDisconnect: () => Promise<void>;
|
||||
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<SignClient>();
|
||||
const [session, setSession] = useState<SessionTypes.Struct>();
|
||||
const [loadingSession, setLoadingSession] = useState(true);
|
||||
const [accounts, setAccounts] = useState<{ address: string }[]>([]);
|
||||
|
||||
const isSignClientInitializing = useRef<boolean>(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 (
|
||||
<ClientContext.Provider
|
||||
value={{
|
||||
signClient,
|
||||
onConnect,
|
||||
onDisconnect,
|
||||
onSessionDelete,
|
||||
loadingSession,
|
||||
session,
|
||||
accounts,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ClientContext.Provider>
|
||||
);
|
||||
};
|
@ -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,7 +31,6 @@ const gqlClient = new GQLClient({ gqlEndpoint });
|
||||
root.render(
|
||||
<LogErrorBoundary>
|
||||
<React.StrictMode>
|
||||
<WalletConnectClientProvider>
|
||||
<ThemeProvider>
|
||||
<Web3ModalProvider>
|
||||
<GQLClientProvider client={gqlClient}>
|
||||
@ -41,7 +39,6 @@ root.render(
|
||||
</GQLClientProvider>
|
||||
</Web3ModalProvider>
|
||||
</ThemeProvider>
|
||||
</WalletConnectClientProvider>
|
||||
</React.StrictMode>
|
||||
</LogErrorBoundary>,
|
||||
);
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user