diff --git a/.env.example b/.env.example index d563968..2e44075 100644 --- a/.env.example +++ b/.env.example @@ -5,4 +5,5 @@ REACT_APP_DEFAULT_GAS_PRICE=0.025 REACT_APP_GAS_ADJUSTMENT=2 REACT_APP_LACONICD_RPC_URL=https://laconicd-sapo.laconic.com -REACT_APP_DEPLOY_APP_URL= +# Example: https://example-url-1.com,https://example-url-2.com +REACT_APP_ALLOWED_URLS= diff --git a/src/App.tsx b/src/App.tsx index ac840a9..201489f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -41,6 +41,7 @@ import { AutoSignIn } from "./screens/AutoSignIn"; import { checkSufficientFunds, getPathKey, sendMessage } from "./utils/misc"; import useAccountsData from "./hooks/useAccountsData"; import { useWebViewHandler } from "./hooks/useWebViewHandler"; +import SignMessageEmbed from "./screens/SignMessageEmbed"; const Stack = createStackNavigator(); @@ -390,6 +391,13 @@ const App = (): React.JSX.Element => { header: () => <>, }} /> +
, + }} + /> { const { networksData } = useNetworks(); const { getAccountsData } = useAccountsData(); @@ -31,6 +33,18 @@ const useGetOrCreateAccounts = () => { const handleCreateAccounts = async (event: MessageEvent) => { if (event.data.type !== 'REQUEST_CREATE_OR_GET_ACCOUNTS') return; + if (!REACT_APP_ALLOWED_URLS) { + console.log('allowed URLs are not set.'); + return; + } + + const allowedUrls = REACT_APP_ALLOWED_URLS.split(',').map(url => url.trim()); + + if (!allowedUrls.includes(event.origin)) { + console.log('Unauthorized app.'); + return; + } + const accountsData = await getOrCreateAccountsForChain(event.data.chainId); sendMessage( @@ -42,7 +56,7 @@ const useGetOrCreateAccounts = () => { const autoCreateAccounts = async () => { const defaultChainId = networksData[0]?.chainId; - + if (!defaultChainId) { console.log('useGetOrCreateAccounts: No default chainId found'); return; @@ -60,7 +74,7 @@ const useGetOrCreateAccounts = () => { window.addEventListener('message', handleCreateAccounts); const isAndroidWebView = !!(window.Android); - + if (isAndroidWebView) { autoCreateAccounts(); } diff --git a/src/hooks/useWebViewHandler.ts b/src/hooks/useWebViewHandler.ts index 9085ecd..7a0c99c 100644 --- a/src/hooks/useWebViewHandler.ts +++ b/src/hooks/useWebViewHandler.ts @@ -40,7 +40,7 @@ export const useWebViewHandler = () => { const path = `/sign/${selectedNetwork.namespace}/${selectedNetwork.chainId}/${currentAccount.address}/${encodeURIComponent(message)}`; const pathRegex = /^\/sign\/(eip155|cosmos)\/(.+)\/(.+)\/(.+)$/; const match = path.match(pathRegex); - + if (!match) { window.Android?.onSignatureError?.('Invalid signing path'); return; diff --git a/src/screens/AutoSignIn.tsx b/src/screens/AutoSignIn.tsx index d9ab05b..f018831 100644 --- a/src/screens/AutoSignIn.tsx +++ b/src/screens/AutoSignIn.tsx @@ -7,6 +7,8 @@ import { sendMessage } from '../utils/misc'; import useAccountsData from '../hooks/useAccountsData'; import useGetOrCreateAccounts from '../hooks/useGetOrCreateAccounts'; +const REACT_APP_ALLOWED_URLS = process.env.REACT_APP_ALLOWED_URLS; + export const AutoSignIn = () => { const { networksData } = useNetworks(); @@ -16,7 +18,14 @@ export const AutoSignIn = () => { const handleSignIn = async (event: MessageEvent) => { if (event.data.type !== 'AUTO_SIGN_IN') return; - if (event.origin !== process.env.REACT_APP_DEPLOY_APP_URL) { + if (!REACT_APP_ALLOWED_URLS) { + console.log('allowed URLs are not set.'); + return; + } + + const allowedUrls = REACT_APP_ALLOWED_URLS.split(',').map(url => url.trim()); + + if (!allowedUrls.includes(event.origin)) { console.log('Unauthorized app.'); return; } diff --git a/src/screens/SignMessageEmbed.tsx b/src/screens/SignMessageEmbed.tsx new file mode 100644 index 0000000..35f6930 --- /dev/null +++ b/src/screens/SignMessageEmbed.tsx @@ -0,0 +1,191 @@ +import React, { useEffect, useState } from 'react'; +import { ScrollView, View } from 'react-native'; +import { ActivityIndicator, Button, Text, Appbar } from 'react-native-paper'; + +import { useNavigation } from '@react-navigation/native'; +import { + NativeStackNavigationProp, + NativeStackScreenProps, +} from '@react-navigation/native-stack'; +import { getHeaderTitle } from '@react-navigation/elements'; + +import { Account, StackParamsList } from '../types'; +import AccountDetails from '../components/AccountDetails'; +import styles from '../styles/stylesheet'; +import { getCosmosAccounts, retrieveSingleAccount } from '../utils/accounts'; +import { getMnemonic, getPathKey, sendMessage } from '../utils/misc'; +import { COSMOS } from '../utils/constants'; + +const REACT_APP_ALLOWED_URLS = process.env.REACT_APP_ALLOWED_URLS; + +type SignRequestProps = NativeStackScreenProps; + +const SignMessageEmbed = ({ route }: SignRequestProps) => { + const [displayAccount, setDisplayAccount] = useState(); + const [message, setMessage] = useState(''); + const [chainId, setChainId] = useState(''); + const [signDoc, setSignDoc] = useState(null); + const [signerAddress, setSignerAddress] = useState(''); + const [origin, setOrigin] = useState(''); + const [sourceWindow, setSourceWindow] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isApproving, setIsApproving] = useState(false); + + const navigation = + useNavigation>(); + + const signMessageHandler = async () => { + if (!signDoc || !signerAddress || !sourceWindow) return; + + setIsApproving(true); + try { + const requestAccount = await retrieveSingleAccount(COSMOS, chainId, signerAddress); + const path = (await getPathKey(`${COSMOS}:${chainId}`, requestAccount!.index)).path; + const mnemonic = await getMnemonic(); + const cosmosAccount = await getCosmosAccounts(mnemonic, path, 'zenith'); + + const cosmosAminoSignature = await cosmosAccount.cosmosWallet.signAmino( + signerAddress, + signDoc, + ); + + const signature = cosmosAminoSignature.signature.signature; + + sendMessage( + sourceWindow, + 'ZENITH_SIGNED_MESSAGE', + { signature }, + origin, + ); + + navigation.navigate('Home'); + } catch (err) { + console.error('Signing failed:', err); + sendMessage( + sourceWindow!, + 'ZENITH_SIGNED_MESSAGE', + { error: err }, + origin, + ); + } finally { + setIsApproving(false); + } + }; + + const rejectRequestHandler = async () => { + if (sourceWindow && origin) { + sendMessage( + sourceWindow, + 'ZENITH_SIGNED_MESSAGE', + { error: 'User rejected the request' }, + origin, + ); + } + navigation.navigate('Home'); + }; + + useEffect(() => { + const handleCosmosSignMessage = async (event: MessageEvent) => { + if (event.data.type !== 'SIGN_ZENITH_MESSAGE') return; + + + if (!REACT_APP_ALLOWED_URLS) { + console.log('allowed URLs are not set.'); + return; + } + + const allowedUrls = REACT_APP_ALLOWED_URLS.split(',').map(url => url.trim()); + + if (!allowedUrls.includes(event.origin)) { + console.log('Unauthorized app.'); + return; + } + + try { + const { signerAddress, signDoc } = event.data.params; + + setSignerAddress(signerAddress); + setSignDoc(signDoc); + setMessage(signDoc.memo || ''); + setOrigin(event.origin); + setSourceWindow(event.source as Window); + setChainId(event.data.chainId); + + const requestAccount = await retrieveSingleAccount( + COSMOS, + event.data.chainId, + signerAddress, + ); + + setDisplayAccount(requestAccount); + setIsLoading(false); + } catch (err) { + console.error('Error preparing sign request:', err); + setIsLoading(false); + } + }; + + window.addEventListener('message', handleCosmosSignMessage); + return () => window.removeEventListener('message', handleCosmosSignMessage); + }, []); + + useEffect(() => { + navigation.setOptions({ + // eslint-disable-next-line react/no-unstable-nested-components + header: ({ options, back }) => { + const title = getHeaderTitle(options, 'Sign Message'); + + return ( + + {back && ( + { + await rejectRequestHandler(); + navigation.navigate('Home'); + }} + /> + )} + + + ); + }, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [navigation]); + + return ( + <> + {isLoading ? ( + + + + ) : ( + <> + + + + {message} + + + + + + + + )} + + ); +}; + +export default SignMessageEmbed; diff --git a/src/types.ts b/src/types.ts index 3b7341e..31d4c3f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -40,6 +40,7 @@ export type StackParamsList = { }; "wallet-embed": undefined; "auto-sign-in": undefined; + "sign-request-embed": undefined; }; export type Account = { diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 8d03e44..5549b7c 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -18,6 +18,18 @@ export const DEFAULT_NETWORKS: NetworksFormData[] = [ gasPrice: '0.001', isDefault: true, }, + { + chainId: 'zenith-testnet', + networkName: 'zenithd testnet', + namespace: COSMOS, + rpcUrl: 'https://zenith-node-rpc.com', + blockExplorerUrl: '', + nativeDenom: 'znt', + addressPrefix: 'zenith', + coinType: '118', + gasPrice: '0.01', + isDefault: true, + }, { chainId: 'laconic_9000-1', networkName: 'laconicd', diff --git a/stack/stack-orchestrator/compose/docker-compose-laconic-wallet-web.yml b/stack/stack-orchestrator/compose/docker-compose-laconic-wallet-web.yml index 9ae5335..79bd086 100644 --- a/stack/stack-orchestrator/compose/docker-compose-laconic-wallet-web.yml +++ b/stack/stack-orchestrator/compose/docker-compose-laconic-wallet-web.yml @@ -10,7 +10,7 @@ services: CERC_DEFAULT_GAS_PRICE: ${CERC_DEFAULT_GAS_PRICE:-0.025} CERC_GAS_ADJUSTMENT: ${CERC_GAS_ADJUSTMENT:-2} CERC_LACONICD_RPC_URL: ${CERC_LACONICD_RPC_URL:-https://laconicd.laconic.com} - CERC_DEPLOY_APP_URL: ${CERC_DEPLOY_APP_URL} + CERC_ALLOWED_URLS: ${CERC_ALLOWED_URLS} command: ["bash", "/scripts/run.sh"] volumes: - ../config/app/run.sh:/scripts/run.sh diff --git a/stack/stack-orchestrator/config/app/run.sh b/stack/stack-orchestrator/config/app/run.sh index 5cdfcfa..72d0e17 100755 --- a/stack/stack-orchestrator/config/app/run.sh +++ b/stack/stack-orchestrator/config/app/run.sh @@ -10,14 +10,14 @@ echo "WALLET_CONNECT_ID: ${WALLET_CONNECT_ID}" echo "CERC_DEFAULT_GAS_PRICE: ${CERC_DEFAULT_GAS_PRICE}" echo "CERC_GAS_ADJUSTMENT: ${CERC_GAS_ADJUSTMENT}" echo "CERC_LACONICD_RPC_URL: ${CERC_LACONICD_RPC_URL}" -echo "CERC_DEPLOY_APP_URL: ${CERC_DEPLOY_APP_URL}" +echo "CERC_ALLOWED_URLS: ${CERC_ALLOWED_URLS}" # Build with required env REACT_APP_WALLET_CONNECT_PROJECT_ID=$WALLET_CONNECT_ID \ REACT_APP_DEFAULT_GAS_PRICE=$CERC_DEFAULT_GAS_PRICE \ REACT_APP_GAS_ADJUSTMENT=$CERC_GAS_ADJUSTMENT \ REACT_APP_LACONICD_RPC_URL=$CERC_LACONICD_RPC_URL \ -REACT_APP_DEPLOY_APP_URL=$CERC_DEPLOY_APP_URL \ +REACT_APP_ALLOWED_URLS=$CERC_ALLOWED_URLS \ yarn build # Define the directory and file path diff --git a/stack/stack-orchestrator/stack/laconic-wallet-web/README.md b/stack/stack-orchestrator/stack/laconic-wallet-web/README.md index 6e0567c..f28151e 100644 --- a/stack/stack-orchestrator/stack/laconic-wallet-web/README.md +++ b/stack/stack-orchestrator/stack/laconic-wallet-web/README.md @@ -49,6 +49,9 @@ Instructions for running the `laconic-wallet-web` using [laconic-so](https://git # WalletConnect project ID, same should be used in the laconic-wallet WALLET_CONNECT_ID= + # Allowed urls is a comma separated list of allowed urls + CERC_ALLOWED_URLS= + # Optional # WalletConnect code for hostname verification @@ -63,10 +66,6 @@ Instructions for running the `laconic-wallet-web` using [laconic-so](https://git # RPC endpoint of laconicd node (default: https://laconicd.laconic.com) CERC_LACONICD_RPC_URL= - - # Deploy app URL used for checking origin of the messages for auto-sign-in route - # Deploy app repo: https://git.vdb.to/cerc-io/snowballtools-base - CERC_DEPLOY_APP_URL= ``` ## Start the deployment