diff --git a/.env.example b/.env.example index b76d155..d563968 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,8 @@ REACT_APP_WALLET_CONNECT_PROJECT_ID= + REACT_APP_DEFAULT_GAS_PRICE=0.025 # Reference: https://github.com/cosmos/cosmos-sdk/issues/16020 REACT_APP_GAS_ADJUSTMENT=2 REACT_APP_LACONICD_RPC_URL=https://laconicd-sapo.laconic.com + +REACT_APP_DEPLOY_APP_URL= diff --git a/src/App.tsx b/src/App.tsx index 1eb7db6..f297288 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,8 @@ import { TxBody, AuthInfo } from "cosmjs-types/cosmos/tx/v1beta1/tx"; import { SignClientTypes } from "@walletconnect/types"; import { useNavigation } from "@react-navigation/native"; +import { DirectSecp256k1Wallet } from "@cosmjs/proto-signing"; +import { SigningStargateClient } from "@cosmjs/stargate"; import { createStackNavigator, StackNavigationProp, @@ -35,6 +37,9 @@ import { COSMOS_METHODS } from "./utils/wallet-connect/COSMOSData"; import styles from "./styles/stylesheet"; import { Header } from "./components/Header"; import { WalletEmbed } from "./screens/WalletEmbed"; +import { AutoSignIn } from "./screens/AutoSignIn"; +import { checkSufficientFunds, getPathKey, sendMessage } from "./utils/misc"; +import useAccountsData from "./hooks/useAccountsData"; const Stack = createStackNavigator(); @@ -44,6 +49,8 @@ const App = (): React.JSX.Element => { const { web3wallet, setActiveSessions } = useWalletConnect(); const { accounts, setCurrentIndex } = useAccounts(); const { networksData, selectedNetwork, setSelectedNetwork } = useNetworks(); + const { getAccountsData } = useAccountsData(); + const [modalVisible, setModalVisible] = useState(false); const [toastVisible, setToastVisible] = useState(false); const [currentProposal, setCurrentProposal] = useState< @@ -218,6 +225,58 @@ const App = (): React.JSX.Element => { }; }); + useEffect(() => { + const handleCheckBalance = async (event: MessageEvent) => { + if (event.data.type !== 'CHECK_BALANCE') return; + + const { chainId, amount } = event.data; + const network = networksData.find(net => net.chainId === chainId); + + if (!network) { + console.error('Network not found'); + throw new Error('Requested network not supported.'); + } + + if (network.namespace !== COSMOS) { + throw new Error('Unsupported network'); + } + + const accounts = await getAccountsData(chainId); + const account = accounts[0]; + + if (!account) { + throw new Error(`No accounts in network ${chainId}`); + } + + + const cosmosPrivKey = ( + await getPathKey(`${network.namespace}:${chainId}`, account.index) + ).privKey; + + const sender = await DirectSecp256k1Wallet.fromKey( + Buffer.from(cosmosPrivKey.split('0x')[1], 'hex'), + network.addressPrefix + ); + + const client = await SigningStargateClient.connectWithSigner(network.rpcUrl!, sender); + + const balance = await client.getBalance( + account.address, + network.nativeDenom!.toLowerCase() + ); + + const areFundsSufficient = checkSufficientFunds(amount, balance.amount); + + sendMessage(event.source as Window, 'IS_SUFFICIENT', areFundsSufficient, event.origin); + }; + + window.addEventListener('message', handleCheckBalance); + + return () => { + window.removeEventListener('message', handleCheckBalance); + }; + }, [networksData, getAccountsData]); + const showWalletConnect = useMemo(() => accounts.length > 0, [accounts]); return ( @@ -321,6 +380,13 @@ const App = (): React.JSX.Element => { header: () => <>, }} /> + <>, + }} + /> { + const { networksData } = useNetworks(); + + const getAccountsData = useCallback(async (chainId: string) => { + const targetNetwork = networksData.find(network => network.chainId === chainId); + + if (!targetNetwork) { + return []; + } + + const accounts = await retrieveAccounts(targetNetwork); + return accounts || []; + }, [networksData]); + + return { getAccountsData }; +}; + +export default useAccountsData; diff --git a/src/hooks/useGetOrCreateAccounts.ts b/src/hooks/useGetOrCreateAccounts.ts new file mode 100644 index 0000000..d9dad1d --- /dev/null +++ b/src/hooks/useGetOrCreateAccounts.ts @@ -0,0 +1,37 @@ +import { useEffect } from "react"; + +import { createWallet } from "../utils/accounts"; +import { sendMessage } from "../utils/misc"; +import useAccountsData from "./useAccountsData"; +import { useNetworks } from "../context/NetworksContext"; + +const useGetOrCreateAccounts = () => { + const { networksData } = useNetworks(); + const { getAccountsData } = useAccountsData(); + + useEffect(() => { + const handleCreateAccounts = async (event: MessageEvent) => { + if (event.data.type !== 'REQUEST_CREATE_OR_GET_ACCOUNTS') return; + + let accountsData = await getAccountsData(event.data.chainId); + + if (accountsData.length === 0) { + console.log("Accounts not found, creating wallet..."); + await createWallet(networksData); + + // Re-fetch newly created accounts + accountsData = await getAccountsData(event.data.chainId); + } + + sendMessage(event.source as Window, 'WALLET_ACCOUNTS_DATA', accountsData, event.origin); + }; + + window.addEventListener('message', handleCreateAccounts); + + return () => { + window.removeEventListener('message', handleCreateAccounts); + }; + }, [networksData, getAccountsData ]); +}; + +export default useGetOrCreateAccounts; diff --git a/src/screens/AutoSignIn.tsx b/src/screens/AutoSignIn.tsx new file mode 100644 index 0000000..d9ab05b --- /dev/null +++ b/src/screens/AutoSignIn.tsx @@ -0,0 +1,49 @@ +import React, { useEffect } from 'react'; + +import { useNetworks } from '../context/NetworksContext'; +import { signMessage } from '../utils/sign-message'; +import { EIP155 } from '../utils/constants'; +import { sendMessage } from '../utils/misc'; +import useAccountsData from '../hooks/useAccountsData'; +import useGetOrCreateAccounts from '../hooks/useGetOrCreateAccounts'; + +export const AutoSignIn = () => { + const { networksData } = useNetworks(); + + const { getAccountsData } = useAccountsData(); + + useEffect(() => { + const handleSignIn = async (event: MessageEvent) => { + if (event.data.type !== 'AUTO_SIGN_IN') return; + + if (event.origin !== process.env.REACT_APP_DEPLOY_APP_URL) { + console.log('Unauthorized app.'); + return; + } + + const accountsData = await getAccountsData(event.data.chainId); + + if (!accountsData.length) { + return + } + + const signature = await signMessage({ message: event.data.message, accountId: accountsData[0].index, chainId: event.data.chainId, namespace: EIP155 }) + + sendMessage(event.source as Window, 'SIGN_IN_RESPONSE', { message: event.data.message, signature }, event.origin); + }; + + window.addEventListener('message', handleSignIn); + + return () => { + window.removeEventListener('message', handleSignIn); + }; + }, [networksData, getAccountsData]); + + // Custom hook for adding listener to get accounts data + useGetOrCreateAccounts(); + + return ( + <> + + ) +}; diff --git a/src/screens/WalletEmbed.tsx b/src/screens/WalletEmbed.tsx index dd0a47f..eb45e76 100644 --- a/src/screens/WalletEmbed.tsx +++ b/src/screens/WalletEmbed.tsx @@ -15,15 +15,17 @@ import { SigningStargateClient, } from '@cosmjs/stargate'; -import { createWallet, retrieveAccounts, retrieveSingleAccount } from '../utils/accounts'; +import { retrieveSingleAccount } from '../utils/accounts'; import AccountDetails from '../components/AccountDetails'; import styles from '../styles/stylesheet'; import DataBox from '../components/DataBox'; -import { getPathKey } from '../utils/misc'; +import { checkSufficientFunds, getPathKey, sendMessage } from '../utils/misc'; import { useNetworks } from '../context/NetworksContext'; import TxErrorDialog from '../components/TxErrorDialog'; import { MEMO } from '../screens/ApproveTransfer'; import { Account, NetworksDataState } from '../types'; +import useGetOrCreateAccounts from '../hooks/useGetOrCreateAccounts'; +import useAccountsData from '../hooks/useAccountsData'; type TransactionDetails = { chainId: string; @@ -45,38 +47,7 @@ export const WalletEmbed = () => { const txEventRef = useRef(null); const { networksData } = useNetworks(); - - const getAccountsData = useCallback(async (chainId: string): Promise => { - const targetNetwork = networksData.find(network => network.chainId === chainId); - - if (!targetNetwork) { - return []; - } - - const accounts = await retrieveAccounts(targetNetwork); - - if (!accounts || accounts.length === 0) { - return []; - } - - return accounts.map(account => account.address); - }, [networksData]); - - const sendMessage = ( - source: Window | null, - type: string, - data: any, - origin: string - ): void => { - source?.postMessage({ type, data }, origin); - }; - - const checkSufficientFunds = (amount: string, balance: string) => { - const amountBigNum = BigNumber.from(String(amount)); - const balanceBigNum = BigNumber.from(balance); - - return balanceBigNum.gt(amountBigNum); - }; + const { getAccountsData } = useAccountsData(); useEffect(() => { const handleGetAccounts = async (event: MessageEvent) => { @@ -99,29 +70,8 @@ export const WalletEmbed = () => { }; }, [getAccountsData]); - useEffect(() => { - const handleCreateAccounts = async (event: MessageEvent) => { - if (event.data.type !== 'REQUEST_CREATE_OR_GET_ACCOUNTS') return; - - let accountsData = await getAccountsData(event.data.chainId); - - if (accountsData.length === 0) { - console.log("Accounts not found, creating wallet..."); - await createWallet(networksData); - - // Re-fetch newly created accounts - accountsData = await getAccountsData(event.data.chainId); - } - - sendMessage(event.source as Window, 'WALLET_ACCOUNTS_DATA', accountsData, event.origin); - }; - - window.addEventListener('message', handleCreateAccounts); - - return () => { - window.removeEventListener('message', handleCreateAccounts); - }; - }, [networksData, getAccountsData]); + // Custom hook for adding listener to get accounts data + useGetOrCreateAccounts(); const handleTxRequested = useCallback( async (event: MessageEvent) => { @@ -342,7 +292,7 @@ export const WalletEmbed = () => { ) : ( - + )} diff --git a/src/types.ts b/src/types.ts index 3ab9c02..95dcb7d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -37,6 +37,7 @@ export type StackParamsList = { requestSessionData: SessionTypes.Struct; }; "wallet-embed": undefined; + "auto-sign-in": undefined; }; export type Account = { diff --git a/src/utils/misc.ts b/src/utils/misc.ts index 5306b84..d7846de 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -1,7 +1,11 @@ /* Importing this library provides react native with a secure random source. For more information, "visit https://docs.ethers.org/v5/cookbook/react-native/#cookbook-reactnative-security" */ import 'react-native-get-random-values'; +import { BigNumber } from 'ethers'; +import { AccountData } from '@cosmjs/amino'; +import { DirectSecp256k1HdWallet } from '@cosmjs/proto-signing'; +import { stringToPath } from '@cosmjs/crypto'; import '@ethersproject/shims'; import { @@ -9,10 +13,6 @@ import { resetInternetCredentials, setInternetCredentials, } from './key-store'; - -import { AccountData } from '@cosmjs/amino'; -import { DirectSecp256k1HdWallet } from '@cosmjs/proto-signing'; -import { stringToPath } from '@cosmjs/crypto'; import { EIP155 } from './constants'; import { NetworksDataState } from '../types'; @@ -149,10 +149,28 @@ const resetKeyServers = async (namespace: string) => { }); }; +const sendMessage = ( + source: Window | null, + type: string, + data: any, + origin: string +): void => { + source?.postMessage({ type, data }, origin); +}; + +const checkSufficientFunds = (amount: string, balance: string) => { + const amountBigNum = BigNumber.from(String(amount)); + const balanceBigNum = BigNumber.from(balance); + + return balanceBigNum.gte(amountBigNum); +}; + export { getMnemonic, getPathKey, updateAccountIndices, getHDPath, resetKeyServers, + sendMessage, + checkSufficientFunds, }; 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 963c3e8..9ae5335 100644 --- a/stack/stack-orchestrator/compose/docker-compose-laconic-wallet-web.yml +++ b/stack/stack-orchestrator/compose/docker-compose-laconic-wallet-web.yml @@ -10,6 +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} 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 77696fe..5cdfcfa 100755 --- a/stack/stack-orchestrator/config/app/run.sh +++ b/stack/stack-orchestrator/config/app/run.sh @@ -10,12 +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}" # 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 \ 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 d44254f..cb340d3 100644 --- a/stack/stack-orchestrator/stack/laconic-wallet-web/README.md +++ b/stack/stack-orchestrator/stack/laconic-wallet-web/README.md @@ -63,6 +63,10 @@ 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 + REACT_APP_DEPLOY_APP_URL= ``` ## Start the deployment