Add route to auto sign in for deploy app (#20)

Part of https://www.notion.so/Simplify-login-flow-in-deploy-laconic-com-190a6b22d47280a9924cc38f8cf4c891

- Auto sign SIWE message received if auto-sign-in is enabled
- Functionality to check account balance

Co-authored-by: Shreerang Kale <shreerangkale@gmail.com>
Co-authored-by: IshaVenikar <ishavenikar7@gmail.com>
Reviewed-on: cerc-io/laconic-wallet-web#20
This commit is contained in:
nabarun 2025-02-12 06:03:09 +00:00
parent 9d2e710632
commit cbb28a6eb3
11 changed files with 216 additions and 62 deletions

View File

@ -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=

View File

@ -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<StackParamsList>();
@ -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: () => <></>,
}}
/>
<Stack.Screen
name="auto-sign-in"
component={AutoSignIn}
options={{
header: () => <></>,
}}
/>
</Stack.Navigator>
<PairingModal
visible={modalVisible}

View File

@ -0,0 +1,23 @@
import { useCallback } from "react";
import { retrieveAccounts } from "../utils/accounts";
import { useNetworks } from "../context/NetworksContext";
const useAccountsData = () => {
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;

View File

@ -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;

View File

@ -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 (
<>
</>
)
};

View File

@ -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<MessageEvent | null>(null);
const { networksData } = useNetworks();
const getAccountsData = useCallback(async (chainId: string): Promise<string[]> => {
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 = () => {
</>
) : (
<View style={styles.spinnerContainer}>
<View style={{marginTop: 50}}></View>
<View style={{ marginTop: 50 }}></View>
<ActivityIndicator size="large" color="#0000ff" />
</View>
)}

View File

@ -37,6 +37,7 @@ export type StackParamsList = {
requestSessionData: SessionTypes.Struct;
};
"wallet-embed": undefined;
"auto-sign-in": undefined;
};
export type Account = {

View File

@ -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,
};

View File

@ -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

View File

@ -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

View File

@ -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