Compare commits

...
Author SHA1 Message Date
nabarunandpranav 2feec2db18 Add method to import account from android using mnemonic (#41)
Part of https://www.notion.so/Integrate-eSIM-buy-flow-into-app-18aa6b22d47280d4a77cf1b27e2ba193

Co-authored-by: pranavjadhav007 <jadhavpranav89@gmail.com>
Reviewed-on: #41
2025-08-02 12:31:01 +00:00
nabarunandpranav 86deb2aab7 Fix balance check for cosmos token transfer txs (#40)
Part of https://www.notion.so/Integrate-eSIM-buy-flow-into-app-18aa6b22d47280d4a77cf1b27e2ba193

Co-authored-by: pranavjadhav007 <jadhavpranav89@gmail.com>
Reviewed-on: #40
2025-07-25 07:50:21 +00:00
nabarunandpranav 96c0a4df06 Add support for wallet_getCapabilities from WalletConnect (#39)
Part of https://www.notion.so/Integrate-eSIM-buy-flow-into-app-18aa6b22d47280d4a77cf1b27e2ba193

- Add Base network
- Check sufficient balance for Eth tx fees

Co-authored-by: pranavjadhav007 <jadhavpranav89@gmail.com>
Reviewed-on: #39
2025-07-15 16:05:02 +00:00
1e88321490 Update wallet code for token transfer using android embedded webview (#36)
Part of https://www.notion.so/Integrate-eSIM-buy-flow-into-app-18aa6b22d47280d4a77cf1b27e2ba193

Co-authored-by: AdityaSalunkhe21 <adityasalunkhe2204@gmail.com>
Co-authored-by: pranavjadhav007 <jadhavpranav89@gmail.com>
Reviewed-on: #36
2025-07-01 04:53:35 +00:00
ishavenikarandIshaVenikar d0623be1c3 Create hook for adding network (#34)
Part of https://www.notion.so/Create-stacks-for-mainnet-1f2a6b22d4728034be4be2c51decf94e

Co-authored-by: IshaVenikar <ishavenikar7@gmail.com>
Reviewed-on: #34
Co-authored-by: ishavenikar <ishavenikar@noreply.git.vdb.to>
Co-committed-by: ishavenikar <ishavenikar@noreply.git.vdb.to>
2025-06-06 10:39:23 +00:00
21 changed files with 709 additions and 316 deletions
-3
View File
@@ -1,11 +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_ZENITHD_RPC_URL=
# Example: https://example-url-1.com,https://example-url-2.com
REACT_APP_ALLOWED_URLS=
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "web-wallet",
"version": "0.1.6",
"version": "0.1.7-nym-0.1.2",
"private": true,
"dependencies": {
"@cerc-io/registry-sdk": "^0.2.5",
+20
View File
@@ -45,6 +45,7 @@ import SignRequestEmbed from "./screens/SignRequestEmbed";
import useAddAccountEmbed from "./hooks/useAddAccountEmbed";
import useExportPKEmbed from "./hooks/useExportPrivateKeyEmbed";
import { SignTxEmbed } from "./screens/SignTxEmbed";
import useCreateNetwork from "./hooks/useCreateNetwork";
const Stack = createStackNavigator<StackParamsList>();
@@ -145,6 +146,23 @@ const App = (): React.JSX.Element => {
requestSessionData,
});
break;
case EIP155_SIGNING_METHODS.WALLET_GET_CAPABILITIES:
const supportedNetworks = networksData
.filter(network => network.namespace === EIP155)
.map(network => `${network.namespace}:${network.chainId}`);
const capabilitiesResponse = formatJsonRpcResult(id, {
accountManagement: true,
sessionManagement: true,
supportedAuthMethods: ['personal_sign', 'eth_sendTransaction'],
supportedNetworks: supportedNetworks,
});
await web3wallet!.respondSessionRequest({
topic,
response: capabilitiesResponse,
});
break;
case COSMOS_METHODS.COSMOS_SIGN_DIRECT:
const message = {
@@ -287,6 +305,7 @@ const App = (): React.JSX.Element => {
useWebViewHandler();
useAddAccountEmbed();
useExportPKEmbed();
useCreateNetwork();
return (
<Surface style={styles.appSurface}>
@@ -345,6 +364,7 @@ const App = (): React.JSX.Element => {
// eslint-disable-next-line react/no-unstable-nested-components
headerRight: () => (
<Button
testID="pair-button"
onPress={() => {
navigation.navigate("AddSession");
}}
+1 -1
View File
@@ -104,7 +104,7 @@ export const Header: React.FC<{
</Stack>
{showWalletConnect && (
<Button onClick={() => navigation.navigate("WalletConnect")}>
<Button data-webviewId="wallet-connect-button" onClick={() => navigation.navigate("WalletConnect")}>
{<WCLogo />}
</Button>
)}
+18 -14
View File
@@ -282,20 +282,24 @@ const PairingModal = ({
)}
</View>
</ScrollView>
<View style={styles.flexRow}>
<Button
mode="contained"
onPress={handleAccept}
loading={isLoading}
disabled={isLoading}>
{isLoading ? 'Connecting' : 'Yes'}
</Button>
<View style={styles.space} />
<Button mode="outlined" onPress={handleReject}>
No
</Button>
</View>
{currentProposal && namespaces && (
<View style={styles.flexRow}>
<Button
mode="contained"
testID="accept-pair-request-button"
onPress={handleAccept}
loading={isLoading}
disabled={isLoading}>
{isLoading ? 'Connecting' : 'Yes'}
</Button>
<View style={styles.space} />
<Button mode="outlined" onPress={handleReject}>
No
</Button>
</View>
)}
</View>
</View>
</Modal>
+24
View File
@@ -14,10 +14,34 @@ declare global {
// Called when accounts are ready for use
onAccountsReady?: () => void;
// Called when transfer is successfully completed
onTransferComplete?: (result: string) => void;
// Called when transfer fails
onTransferError?: (error: string) => void;
// Called when transfer is cancelled
onTransferCancelled?: () => void;
// Called when account is created
onAccountCreated?: (account: string) => void;
// Called when account creation fails
onAccountError?: (error: string) => void;
};
// Handles incoming signature requests from Android
receiveSignRequestFromAndroid?: (message: string) => void;
// Handles incoming transfer requests from Android
receiveTransferRequestFromAndroid?: (to: string, amount: string, namespace: String, chainId: string, memo: string) => void;
// Handles account creation requests from Android
receiveGetOrCreateAccountFromAndroid?: (chainId: string) => void;
// Handles account import requests from Android
receiveImportAccountFromAndroid?: (chainId: string, mnemonic: string) => void;
}
}
+99
View File
@@ -0,0 +1,99 @@
import { useEffect, useCallback } from "react";
import { addNewNetwork, createWallet, checkNetworkForChainID, isWalletCreated } from "../utils/accounts";
import { useNetworks } from "../context/NetworksContext";
import { NETWORK_ADDED_RESPONSE, NETWORK_ADD_FAILED_RESPONSE, NETWORK_ALREADY_EXISTS_RESPONSE, REQUEST_ADD_NETWORK } from "../utils/constants";
import { NetworksFormData } from "../types";
import { sendMessage } from "../utils/misc";
const REACT_APP_ALLOWED_URLS = import.meta.env.REACT_APP_ALLOWED_URLS;
const useCreateNetwork = () => {
const { networksData, setNetworksData } = useNetworks();
const getOrCreateNetwork = useCallback(
async (chainId: string, networkData: NetworksFormData, sourceOrigin?: string) => {
if (!sourceOrigin) {
return;
}
try {
const isCreated = await isWalletCreated();
if (!isCreated) {
console.log("Wallet not found, creating wallet...");
await createWallet(networksData);
}
const isNetworkPresent = await checkNetworkForChainID(chainId);
if (!isNetworkPresent) {
console.log("ChainId not found. Adding network");
const resolvedNetworkData = {
chainId,
namespace: networkData.namespace,
networkName: networkData.networkName,
rpcUrl: networkData.rpcUrl,
blockExplorerUrl: networkData.blockExplorerUrl || "",
addressPrefix: networkData.addressPrefix || "",
coinType: networkData.coinType,
nativeDenom: networkData.nativeDenom || "",
gasPrice: networkData.gasPrice || String(import.meta.env.REACT_APP_DEFAULT_GAS_PRICE),
currencySymbol: networkData.currencySymbol || "",
isDefault: false,
};
const retrievedNetworksData = await addNewNetwork(resolvedNetworkData);
setNetworksData(retrievedNetworksData);
sendMessage(window.parent, NETWORK_ADDED_RESPONSE, {
type: NETWORK_ADDED_RESPONSE,
chainId
}, sourceOrigin);
} else {
sendMessage(window.parent, NETWORK_ALREADY_EXISTS_RESPONSE, {
type: NETWORK_ALREADY_EXISTS_RESPONSE,
chainId
}, sourceOrigin);
}
} catch (error) {
console.error("Error in getOrCreateNetwork:", error);
sendMessage(window.parent, NETWORK_ADD_FAILED_RESPONSE, {
type: NETWORK_ADD_FAILED_RESPONSE,
message: error instanceof Error ? error.message : "Unknown error"
}, sourceOrigin);
}
},
[networksData, setNetworksData]
);
useEffect(() => {
const handleCreateNetwork = async (event: MessageEvent) => {
if (event.data.type !== REQUEST_ADD_NETWORK) 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 { chainId, networkData } = event.data;
await getOrCreateNetwork(chainId, networkData, event.origin);
};
window.addEventListener("message", handleCreateNetwork);
return () => {
window.removeEventListener("message", handleCreateNetwork);
};
}, [getOrCreateNetwork]);
};
export default useCreateNetwork;
+7 -27
View File
@@ -1,6 +1,6 @@
import { useEffect, useCallback } from "react";
import { createWallet } from "../utils/accounts";
import { createWallet, isWalletCreated } from "../utils/accounts";
import { sendMessage } from "../utils/misc";
import useAccountsData from "./useAccountsData";
import { useNetworks } from "../context/NetworksContext";
@@ -16,14 +16,15 @@ const useGetOrCreateAccounts = () => {
// Wrap the function in useCallback to prevent recreation on each render
const getOrCreateAccountsForChain = useCallback(async (chainId: string) => {
let accountsData = await getAccountsData(chainId);
const isCreated = await isWalletCreated();
if (accountsData.length === 0) {
if (!isCreated) {
console.log("Accounts not found, creating wallet...");
await createWallet(networksData);
accountsData = await getAccountsData(chainId);
}
const accountsData = await getAccountsData(chainId);
// Update the AccountsContext with the new accounts
setAccounts(accountsData);
@@ -57,35 +58,14 @@ const useGetOrCreateAccounts = () => {
);
};
const autoCreateAccounts = async () => {
const defaultChainId = networksData[0]?.chainId;
if (!defaultChainId) {
console.log('useGetOrCreateAccounts: No default chainId found');
return;
}
const accounts = await getOrCreateAccountsForChain(defaultChainId);
// Only notify Android when we actually have accounts
if (accounts.length > 0 && window.Android?.onAccountsReady) {
window.Android.onAccountsReady();
} else {
console.log('No accounts created or Android bridge not available');
}
};
window.addEventListener('message', handleCreateAccounts);
const isAndroidWebView = !!(window.Android);
if (isAndroidWebView) {
autoCreateAccounts();
}
return () => {
window.removeEventListener('message', handleCreateAccounts);
};
}, [networksData, getAccountsData, getOrCreateAccountsForChain]);
return { getOrCreateAccountsForChain };
};
export default useGetOrCreateAccounts;
+135 -4
View File
@@ -4,17 +4,87 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useAccounts } from '../context/AccountsContext';
import { useNetworks } from '../context/NetworksContext';
import { StackParamsList } from '../types';
import useGetOrCreateAccounts from './useGetOrCreateAccounts';
import { retrieveAccountsForNetwork, createWallet, retrieveNetworksData, isWalletCreated } from '../utils/accounts';
export const useWebViewHandler = () => {
// Navigation and context hooks
const navigation = useNavigation<NativeStackNavigationProp<StackParamsList>>();
const { selectedNetwork } = useNetworks();
const { accounts, currentIndex } = useAccounts();
const { getOrCreateAccountsForChain } = useGetOrCreateAccounts();
const { selectedNetwork, setNetworksData, setSelectedNetwork } = useNetworks();
const { accounts, currentIndex, setAccounts } = useAccounts();
// Initialize accounts
useGetOrCreateAccounts();
const handleGetOrCreateAccount = useCallback(async (chainId: string) => {
try {
const accountsData = await getOrCreateAccountsForChain(chainId);
if (!accountsData || accountsData.length === 0) {
window.Android?.onAccountError?.('Failed to create/retrieve account');
return;
}
window.Android?.onAccountCreated?.(JSON.stringify(accountsData[0]));
} catch (error) {
console.error('Account operation error:', error);
window.Android?.onAccountError?.(`Operation failed: ${error}`);
}
}, [getOrCreateAccountsForChain]);
// Import account using mnemonic
const handleImportAccount = useCallback(async (chainId: string, mnemonic: string) => {
try {
// Get available networks data
const networksData = await retrieveNetworksData();
if (!networksData || networksData.length === 0) {
window.Android?.onAccountError?.('No networks configured');
return;
}
// Create wallet using the provided mnemonic if it doesn't exist
const walletExists = await isWalletCreated();
if (!walletExists) {
await createWallet(networksData, mnemonic);
}
// Find the requested network
const requestedNetwork = networksData.find(network => network.chainId === chainId);
if (!requestedNetwork) {
window.Android?.onAccountError?.(`Network with chainId '${chainId}' not found`);
return;
}
// Update networks context
setNetworksData(networksData);
setSelectedNetwork(requestedNetwork);
// Retrieve accounts for the requested network
const accounts = await retrieveAccountsForNetwork(
`${requestedNetwork.namespace}:${requestedNetwork.chainId}`,
'0'
);
if (!accounts || accounts.length === 0) {
window.Android?.onAccountError?.('Failed to import account');
return;
}
// Update accounts context
setAccounts(accounts);
// Notify Android that account was successfully imported for the requested chain
window.Android?.onAccountCreated?.(JSON.stringify(accounts[0]));
} catch (error) {
console.error('Import account error:', error);
window.Android?.onAccountError?.(`Import failed: ${error}`);
}
}, [setNetworksData, setSelectedNetwork, setAccounts]);
// Core navigation handler
const navigateToSignRequest = useCallback((message: string) => {
@@ -27,12 +97,14 @@ export const useWebViewHandler = () => {
if (!accounts?.length) {
window.Android?.onSignatureError?.('No accounts available');
return;
}
const currentAccount = accounts[currentIndex];
if (!currentAccount) {
window.Android?.onSignatureError?.('Current account not found');
return;
}
@@ -43,6 +115,7 @@ export const useWebViewHandler = () => {
if (!match) {
window.Android?.onSignatureError?.('Invalid signing path');
return;
}
@@ -70,12 +143,70 @@ export const useWebViewHandler = () => {
}
}, [selectedNetwork, accounts, currentIndex, navigation]);
// Handle incoming transfer requests
const navigateToTransfer = useCallback(async (to: string, amount: string, namespace: String, chainId: string, memo: string) => {
try {
// TODO: Pass the account info for transferring tokens
// Get first account
const [chainAccount] = await retrieveAccountsForNetwork(
`${namespace}:${chainId}`,
'0'
);
if (!chainAccount) {
console.error('Accounts not found');
if (window.Android?.onTransferError) {
window.Android.onTransferError('Accounts not found');
}
return;
}
const path = `/transfer/${namespace}/${chainId}/${chainAccount.address}/${to}/${amount}`;
navigation.reset({
index: 0,
routes: [
{
name: 'ApproveTransfer',
path: path,
params: {
namespace: namespace,
chainId: `${namespace}:${chainId}`,
transaction: {
from: chainAccount.address,
to: to,
value: amount
},
accountInfo: chainAccount,
memo: memo
},
},
],
});
} catch (error) {
if (window.Android?.onTransferError) {
window.Android.onTransferError(`Navigation error: ${error}`);
}
}
}, [navigation]);
useEffect(() => {
// Assign the function to the window object
window.receiveSignRequestFromAndroid = navigateToSignRequest;
window.receiveTransferRequestFromAndroid = navigateToTransfer;
window.receiveGetOrCreateAccountFromAndroid = handleGetOrCreateAccount;
window.receiveImportAccountFromAndroid = handleImportAccount;
return () => {
window.receiveSignRequestFromAndroid = undefined;
window.receiveTransferRequestFromAndroid = undefined;
window.receiveGetOrCreateAccountFromAndroid = undefined;
window.receiveImportAccountFromAndroid = undefined;
};
}, [navigateToSignRequest]); // Only the function reference as dependency
}, [navigateToSignRequest, navigateToTransfer, handleGetOrCreateAccount, handleImportAccount]); // Only the function reference as dependency
};
-1
View File
@@ -5,7 +5,6 @@ interface ImportMetaEnv {
readonly REACT_APP_DEFAULT_GAS_PRICE: string;
readonly REACT_APP_GAS_ADJUSTMENT: string;
readonly REACT_APP_LACONICD_RPC_URL: string;
readonly REACT_APP_ZENITHD_RPC_URL: string;
readonly REACT_APP_ALLOWED_URLS: string;
}
+4 -75
View File
@@ -1,8 +1,6 @@
import React, { useCallback, useEffect, useState } from "react";
import { useForm, Controller, useWatch, FieldErrors } from "react-hook-form";
import { TextInput, HelperText } from "react-native-paper";
import { HDNode } from "ethers/lib/utils";
import { chains } from "chain-registry";
import { useDebouncedCallback } from "use-debounce";
import { z } from "zod";
@@ -10,27 +8,21 @@ import { z } from "zod";
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { useNavigation } from "@react-navigation/native";
import { zodResolver } from "@hookform/resolvers/zod";
import { Divider, Grid } from "@mui/material";
import { LoadingButton } from "@mui/lab";
import { StackParamsList } from "../types";
import { SelectNetworkType } from "../components/SelectNetworkType";
import { addAccountsForNetwork, getNextAccountId, storeNetworkData } from "../utils/accounts";
import { addNewNetwork } from "../utils/accounts";
import { useNetworks } from "../context/NetworksContext";
import {
COSMOS,
EIP155,
CHAINID_DEBOUNCE_DELAY,
EMPTY_FIELD_ERROR,
INVALID_URL_ERROR,
IS_NUMBER_REGEX,
} from "../utils/constants";
import { getCosmosAccountByHDPath } from "../utils/accounts";
import ETH_CHAINS from "../assets/ethereum-chains.json";
import {
getInternetCredentials,
setInternetCredentials,
} from "../utils/key-store";
import { Divider, Grid } from "@mui/material";
import { LoadingButton } from "@mui/lab";
import { Layout } from "../components/Layout";
const ethNetworkDataSchema = z.object({
@@ -143,73 +135,10 @@ const AddNetwork = () => {
isDefault: false,
};
const mnemonicServer = await getInternetCredentials("mnemonicServer");
const mnemonic = mnemonicServer;
const retrievedNetworksData = await addNewNetwork(newNetworkData);
if (!mnemonic) {
throw new Error("Mnemonic not found");
}
const hdNode = HDNode.fromMnemonic(mnemonic);
const hdPath = `m/44'/${newNetworkData.coinType}'/0'/0/0`;
const node = hdNode.derivePath(hdPath);
let address;
switch (newNetworkData.namespace) {
case EIP155:
address = node.address;
break;
case COSMOS:
address = (
await getCosmosAccountByHDPath(
mnemonic,
hdPath,
(newNetworkData as z.infer<typeof cosmosNetworkDataSchema>)
.addressPrefix,
)
).data.address;
break;
default:
throw new Error("Unsupported namespace");
}
const accountInfo = `${hdPath},${node.privateKey},${node.publicKey},${address}`;
const retrievedNetworksData = await storeNetworkData(newNetworkData);
setNetworksData(retrievedNetworksData);
// Get number of accounts in first network
const nextAccountId = await getNextAccountId(
`${retrievedNetworksData[0].namespace}:${retrievedNetworksData[0].chainId}`,
);
const selectedNetwork = retrievedNetworksData.find(
(network) => network.chainId === newNetworkData.chainId,
);
await Promise.all([
setInternetCredentials(
`accounts/${newNetworkData.namespace}:${newNetworkData.chainId}/0`,
"_",
accountInfo,
),
setInternetCredentials(
`addAccountCounter/${newNetworkData.namespace}:${newNetworkData.chainId}`,
"_",
"1",
),
setInternetCredentials(
`accountIndices/${newNetworkData.namespace}:${newNetworkData.chainId}`,
"_",
"0",
),
]);
await addAccountsForNetwork(selectedNetwork!, nextAccountId - 1);
navigation.navigate("Home");
},
[navigation, namespace, setNetworksData],
+1 -1
View File
@@ -44,7 +44,7 @@ const AddSession = () => {
/>
<Box sx={{ mt: 2 }}>
<Button variant="contained" onClick={pair}>
<Button variant="contained" data-webviewId="pair-session-button" onClick={pair}>
Pair Session
</Button>
</Box>
+229 -133
View File
@@ -2,11 +2,11 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Image, ScrollView, View } from 'react-native';
import {
ActivityIndicator,
Button,
Text,
Appbar,
TextInput,
} from 'react-native-paper';
import JSONbig from 'json-bigint';
import { providers, BigNumber } from 'ethers';
import { Deferrable } from 'ethers/lib/utils';
@@ -41,28 +41,29 @@ import { COSMOS, EIP155, IS_NUMBER_REGEX } from '../utils/constants';
import TxErrorDialog from '../components/TxErrorDialog';
import { EIP155_SIGNING_METHODS } from '../utils/wallet-connect/EIP155Data';
import { COSMOS_METHODS } from '../utils/wallet-connect/COSMOSData';
import { Button } from '@mui/material';
import { LoadingButton } from '@mui/lab';
export const MEMO = 'Sending signed tx from Laconic Wallet';
// Reference: https://ethereum.org/en/developers/docs/gas/#what-is-gas-limit
const ETH_MINIMUM_GAS = 21000;
type SignRequestProps = NativeStackScreenProps<
StackParamsList,
'ApproveTransfer'
>;
type ApproveTransferProps = NativeStackScreenProps<StackParamsList, 'ApproveTransfer'>
const ApproveTransfer = ({ route }: SignRequestProps) => {
const ApproveTransfer = ({ route }: ApproveTransferProps) => {
const { networksData } = useNetworks();
const { web3wallet } = useWalletConnect();
// Extract data from route params or path
const requestSession = route.params.requestSessionData;
const requestName = requestSession.peer.metadata.name;
const requestIcon = requestSession.peer.metadata.icons[0];
const requestURL = requestSession.peer.metadata.url;
const requestName = requestSession?.peer.metadata.name;
const requestIcon = requestSession?.peer.metadata.icons[0];
const requestURL = requestSession?.peer.metadata.url;
const transaction = route.params.transaction;
const requestEvent = route.params.requestEvent;
const chainId = requestEvent.params.chainId;
const requestMethod = requestEvent.params.request.method;
const chainId = requestEvent?.params.chainId || route.params.chainId;
const requestMethod = requestEvent?.params.request.method;
const txMemo = route.params.memo || MEMO;
const [account, setAccount] = useState<Account>();
const [isLoading, setIsLoading] = useState(true);
@@ -81,23 +82,27 @@ const ApproveTransfer = ({ route }: SignRequestProps) => {
useState<BigNumber | null>();
const isSufficientFunds = useMemo(() => {
if (!transaction.value) {
return;
}
if (!balance) {
return;
}
const amountBigNum = BigNumber.from(String(transaction.value));
const balanceBigNum = BigNumber.from(balance);
if (amountBigNum.gte(balanceBigNum)) {
return false;
} else {
return true;
if (!fees) {
return;
}
}, [balance, transaction]);
const balanceBigNum = BigNumber.from(balance);
const feesBigNum = BigNumber.from(fees);
let totalRequiredBigNum = feesBigNum;
if (transaction.value) {
const amountBigNum = BigNumber.from(String(transaction.value));
totalRequiredBigNum = amountBigNum.add(feesBigNum);
}
// Compare the user's balance with the total required amount
const isSufficient = balanceBigNum.gte(totalRequiredBigNum);
return isSufficient;
}, [balance, transaction.value, fees]);
const requestedNetwork = networksData.find(
networkData =>
@@ -205,6 +210,58 @@ const ApproveTransfer = ({ route }: SignRequestProps) => {
[navigation, requestedNetwork],
);
//TODO: Handle ETH transactions
const handleIntent = async () => {
if (!account) {
throw new Error('Account is not valid');
}
console.log('Sending transaction request:', {
from: account.address,
to: transaction.to,
amount: transaction.value,
denom: requestedNetwork!.nativeDenom,
memo: txMemo,
gas: cosmosGasLimit,
fees: fees
});
if (!requestedNetwork) {
throw new Error('Network not found');
}
if (!cosmosStargateClient) {
throw new Error('Cosmos stargate client not found');
}
const result = await cosmosStargateClient.signAndBroadcast(
account.address,
[sendMsg],
{
amount: [
{
amount: fees,
denom: requestedNetwork.nativeDenom!,
},
],
gas: cosmosGasLimit,
},
txMemo,
);
console.log('Transaction result:', result);
// Convert BigInt values to strings before sending to Android
const serializedResult = JSONbig.stringify(result);
// Send the result back to Android and close dialog
if (window.Android?.onTransferComplete) {
window.Android.onTransferComplete(serializedResult);
} else {
alert(`Transaction: ${serializedResult}`);
}
};
useEffect(() => {
// Set loading to false when gas values for requested chain are fetched
// If requested chain is EVM compatible, the cosmos gas values will be undefined and vice-versa, hence the condition checks only one of them at the same time
@@ -220,8 +277,12 @@ const ApproveTransfer = ({ route }: SignRequestProps) => {
useEffect(() => {
if (namespace === EIP155) {
const ethFees = BigNumber.from(ethGasLimit ?? 0)
.mul(BigNumber.from(ethMaxFee ?? ethGasPrice ?? 0))
if (!ethGasLimit || !(ethMaxFee || ethGasPrice)){
return;
}
const ethFees = BigNumber.from(ethGasLimit)
.mul(BigNumber.from(ethMaxFee ?? ethGasPrice))
.toString();
setFees(ethFees);
} else {
@@ -246,6 +307,7 @@ const ApproveTransfer = ({ route }: SignRequestProps) => {
requestedNetwork,
ethMaxFee,
]);
useEffect(() => {
retrieveData(transaction.from!);
}, [retrieveData, transaction]);
@@ -267,78 +329,82 @@ const ApproveTransfer = ({ route }: SignRequestProps) => {
throw new Error('account not found');
}
if (ethGasLimit && ethGasLimit.lt(ETH_MINIMUM_GAS)) {
throw new Error(`Atleast ${ETH_MINIMUM_GAS} gas limit is required`);
}
if (requestEvent) {
// Handle WalletConnect request
if (ethGasLimit && ethGasLimit.lt(ETH_MINIMUM_GAS)) {
throw new Error(`Atleast ${ETH_MINIMUM_GAS} gas limit is required`);
}
if (ethMaxFee && ethMaxPriorityFee && ethMaxFee.lte(ethMaxPriorityFee)) {
throw new Error(
`Max fee per gas (${ethMaxFee.toNumber()}) cannot be lower than or equal to max priority fee per gas (${ethMaxPriorityFee.toNumber()})`,
if (ethMaxFee && ethMaxPriorityFee && ethMaxFee.lte(ethMaxPriorityFee)) {
throw new Error(
`Max fee per gas (${ethMaxFee.toNumber()}) cannot be lower than or equal to max priority fee per gas (${ethMaxPriorityFee.toNumber()})`,
);
}
let options: WalletConnectRequests;
switch (requestMethod) {
case EIP155_SIGNING_METHODS.ETH_SEND_TRANSACTION:
if (
ethMaxFee === undefined ||
ethMaxPriorityFee === undefined ||
ethGasPrice === undefined
) {
throw new Error('Gas values not found');
}
options = {
type: 'eth_sendTransaction',
provider: provider!,
ethGasLimit: BigNumber.from(ethGasLimit),
ethGasPrice: ethGasPrice ? ethGasPrice.toHexString() : null,
maxFeePerGas: ethMaxFee,
maxPriorityFeePerGas: ethMaxPriorityFee,
};
break;
case COSMOS_METHODS.COSMOS_SEND_TOKENS:
if (!cosmosStargateClient) {
throw new Error('Cosmos stargate client not found');
}
options = {
type: 'cosmos_sendTokens',
signingStargateClient: cosmosStargateClient,
cosmosFee: {
amount: [
{
amount: fees,
denom: requestedNetwork!.nativeDenom!,
},
],
gas: cosmosGasLimit,
},
sendMsg,
memo: txMemo,
};
break;
default:
throw new Error('Invalid method');
}
const response = await approveWalletConnectRequest(
requestEvent,
account!,
namespace,
requestedNetwork!.chainId,
options,
);
const { topic } = requestEvent;
await web3wallet!.respondSessionRequest({ topic, response });
navigation.navigate('Home');
} else {
await handleIntent();
}
let options: WalletConnectRequests;
switch (requestMethod) {
case EIP155_SIGNING_METHODS.ETH_SEND_TRANSACTION:
if (
ethMaxFee === undefined ||
ethMaxPriorityFee === undefined ||
ethGasPrice === undefined
) {
throw new Error('Gas values not found');
}
options = {
type: 'eth_sendTransaction',
provider: provider!,
ethGasLimit: BigNumber.from(ethGasLimit),
ethGasPrice: ethGasPrice ? ethGasPrice.toHexString() : null,
maxFeePerGas: ethMaxFee,
maxPriorityFeePerGas: ethMaxPriorityFee,
};
break;
case COSMOS_METHODS.COSMOS_SEND_TOKENS:
if (!cosmosStargateClient) {
throw new Error('Cosmos stargate client not found');
}
options = {
type: 'cosmos_sendTokens',
signingStargateClient: cosmosStargateClient,
// StdFee object
cosmosFee: {
// This amount is total fees required for transaction
amount: [
{
amount: fees,
denom: requestedNetwork!.nativeDenom!,
},
],
gas: cosmosGasLimit,
},
sendMsg,
memo: MEMO,
};
break;
default:
throw new Error('Invalid method');
}
const response = await approveWalletConnectRequest(
requestEvent,
account,
namespace,
requestedNetwork!.chainId,
options,
);
const { topic } = requestEvent;
await web3wallet!.respondSessionRequest({ topic, response });
navigation.navigate('Home');
} catch (error) {
if (window.Android?.onTransferError) {
window.Android.onTransferError(`Transaction Failed: ${error}`);
}
if (!(error instanceof Error)) {
throw error;
}
@@ -350,14 +416,31 @@ const ApproveTransfer = ({ route }: SignRequestProps) => {
};
const rejectRequestHandler = async () => {
const response = rejectWalletConnectRequest(requestEvent);
const { topic } = requestEvent;
await web3wallet!.respondSessionRequest({
topic,
response,
});
setIsTxLoading(true);
try {
if (requestEvent) {
const response = rejectWalletConnectRequest(requestEvent);
const { topic } = requestEvent;
await web3wallet!.respondSessionRequest({
topic,
response,
});
}
navigation.navigate('Home');
if (window.Android?.onTransferCancelled) {
window.Android.onTransferCancelled();
} else {
navigation.navigate('Home');
}
} catch (error) {
if (!(error instanceof Error)) {
throw error;
}
setTxError(error.message);
setIsTxErrorDialogOpen(true);
}
setIsTxLoading(false);
};
useEffect(() => {
@@ -420,7 +503,7 @@ const ApproveTransfer = ({ route }: SignRequestProps) => {
useEffect(() => {
const getEthGas = async () => {
try {
if (!isSufficientFunds || !provider) {
if (!provider) {
return;
}
@@ -465,14 +548,14 @@ const ApproveTransfer = ({ route }: SignRequestProps) => {
if (!cosmosStargateClient) {
return;
}
if (!isSufficientFunds) {
if (!balance) {
return;
}
const gasEstimation = await cosmosStargateClient.simulate(
transaction.from!,
[sendMsg],
MEMO,
txMemo,
);
setCosmosGasLimit(
@@ -484,20 +567,22 @@ const ApproveTransfer = ({ route }: SignRequestProps) => {
if (!(error instanceof Error)) {
throw error;
}
if (window.Android?.onTransferError) {
window.Android.onTransferError(`Not able to estimate gas`);
}
setTxError(error.message);
setIsTxErrorDialogOpen(true);
}
};
getCosmosGas();
}, [cosmosStargateClient, isSufficientFunds, sendMsg, transaction]);
}, [cosmosStargateClient, balance, sendMsg, transaction, txMemo]);
useEffect(() => {
if (balance && !isSufficientFunds) {
if (balance && !isSufficientFunds && !fees) {
setTxError('Insufficient funds');
setIsTxErrorDialogOpen(true);
}
}, [isSufficientFunds, balance]);
}, [isSufficientFunds, balance, fees]);
return (
<>
@@ -508,16 +593,18 @@ const ApproveTransfer = ({ route }: SignRequestProps) => {
) : (
<>
<ScrollView contentContainerStyle={styles.appContainer}>
<View style={styles.dappDetails}>
{requestIcon && (
<Image
style={styles.dappLogo}
source={requestIcon ? { uri: requestIcon } : undefined}
/>
)}
<Text>{requestName}</Text>
<Text variant="bodyMedium">{requestURL}</Text>
</View>
{requestSession && (
<View style={styles.dappDetails}>
{requestIcon && (
<Image
style={styles.dappLogo}
source={requestIcon ? { uri: requestIcon } : undefined}
/>
)}
<Text>{requestName}</Text>
<Text variant="bodyMedium">{requestURL}</Text>
</View>
)}
<View style={styles.dataBoxContainer}>
<Text style={styles.dataBoxLabel}>From</Text>
<View style={styles.dataBox}>
@@ -537,14 +624,22 @@ const ApproveTransfer = ({ route }: SignRequestProps) => {
{transaction && (
<View style={styles.approveTransfer}>
<DataBox label="To" data={transaction.to!} />
<DataBox
label={`Amount (${
namespace === EIP155 ? 'wei' : requestedNetwork!.nativeDenom
})`}
data={BigNumber.from(
transaction.value?.toString(),
).toString()}
/>
{transaction.value !== undefined && transaction.value !== null && (
<DataBox
label={`Amount (${
namespace === EIP155 ? 'wei' : requestedNetwork!.nativeDenom
})`}
data={BigNumber.from(
transaction.value?.toString(),
).toString()}
/>
)}
{namespace === COSMOS && (
<DataBox
label="Memo"
data={txMemo}
/>
)}
{namespace === EIP155 ? (
<>
@@ -638,17 +733,18 @@ const ApproveTransfer = ({ route }: SignRequestProps) => {
)}
</ScrollView>
<View style={styles.buttonContainer}>
<Button
mode="contained"
onPress={acceptRequestHandler}
<LoadingButton
variant="contained"
onClick={acceptRequestHandler}
loading={isTxLoading}
disabled={!balance || !fees}>
disabled={!balance || !fees}
id="approve-transaction-button">
{isTxLoading ? 'Processing' : 'Yes'}
</Button>
</LoadingButton>
<Button
mode="contained"
onPress={rejectRequestHandler}
buttonColor="#B82B0D">
variant="contained"
onClick={rejectRequestHandler}
color="error">
No
</Button>
</View>
+3 -2
View File
@@ -116,7 +116,8 @@ const SignRequest = ({ route }: SignRequestProps) => {
);
useEffect(() => {
if (route.path) {
const requestEvent = route.params.requestEvent;
if (route.path && !requestEvent) {
const sanitizedRoute = sanitizePath(route.path);
sanitizedRoute &&
retrieveData(
@@ -127,7 +128,6 @@ const SignRequest = ({ route }: SignRequestProps) => {
);
return;
}
const requestEvent = route.params.requestEvent;
const requestChainId = requestEvent?.params.chainId;
const requestedChain = networksData.find(
@@ -310,6 +310,7 @@ const SignRequest = ({ route }: SignRequestProps) => {
<View style={styles.buttonContainer}>
<Button
mode="contained"
testID="accept-sign-request-button"
onPress={signMessageHandler}
loading={isApproving}
disabled={isApproving}>
+13 -8
View File
@@ -43,15 +43,20 @@ export default function WalletConnect() {
// eslint-disable-next-line react/no-unstable-nested-components
left={() => (
<>
{session.peer.metadata.icons[0].endsWith(".svg") ? (
<View style={styles.dappLogo}>
<Text>SvgURI peerMetaDataIcon</Text>
</View>
{session.peer.metadata.icons && session.peer.metadata.icons.length > 0 ? (
session.peer.metadata.icons[0].endsWith(".svg") ? (
<View style={styles.dappLogo}>
<Text>SvgURI peerMetaDataIcon</Text>
</View>
) : (
<Image
style={styles.dappLogo}
source={{ uri: session.peer.metadata.icons[0] }}
/>
)
) : (
<Image
style={styles.dappLogo}
source={{ uri: session.peer.metadata.icons[0] }}
/>
// Render nothing if no icon is available
<View style={styles.dappLogo} /> // Or simply null
)}
</>
)}
+5 -3
View File
@@ -21,9 +21,11 @@ export type StackParamsList = {
requestSessionData?: SessionTypes.Struct;
};
ApproveTransfer: {
chainId?: string;
transaction: PopulatedTransaction;
requestEvent: Web3WalletTypes.SessionRequest;
requestSessionData: SessionTypes.Struct;
requestEvent?: Web3WalletTypes.SessionRequest;
requestSessionData?: SessionTypes.Struct;
memo?: string;
};
InvalidPath: undefined;
WalletConnect: undefined;
@@ -64,7 +66,7 @@ export type NetworksFormData = {
namespace: string;
nativeDenom?: string;
addressPrefix?: string;
coinType?: string;
coinType: string;
gasPrice?: string;
isDefault: boolean;
};
+104 -2
View File
@@ -160,6 +160,77 @@ const addAccountFromHDPath = async (
}
};
const addNewNetwork = async (
newNetworkData: NetworksFormData
): Promise<NetworksDataState[]> => {
const mnemonicServer = await getInternetCredentials("mnemonicServer");
const mnemonic = mnemonicServer;
if (!mnemonic) {
throw new Error("Mnemonic not found");
}
const hdNode = HDNode.fromMnemonic(mnemonic);
const hdPath = `m/44'/${newNetworkData.coinType}'/0'/0/0`;
const node = hdNode.derivePath(hdPath);
let address;
switch (newNetworkData.namespace) {
case EIP155:
address = node.address;
break;
case COSMOS:
address = (
await getCosmosAccountByHDPath(
mnemonic,
hdPath,
newNetworkData.addressPrefix,
)
).data.address;
break;
default:
throw new Error("Unsupported namespace");
}
const accountInfo = `${hdPath},${node.privateKey},${node.publicKey},${address}`;
await Promise.all([
setInternetCredentials(
`accounts/${newNetworkData.namespace}:${newNetworkData.chainId}/0`,
"_",
accountInfo,
),
setInternetCredentials(
`addAccountCounter/${newNetworkData.namespace}:${newNetworkData.chainId}`,
"_",
"1",
),
setInternetCredentials(
`accountIndices/${newNetworkData.namespace}:${newNetworkData.chainId}`,
"_",
"0",
),
]);
const retrievedNetworksData = await storeNetworkData(newNetworkData);
// Get number of accounts in first network
const nextAccountId = await getNextAccountId(
`${retrievedNetworksData[0].namespace}:${retrievedNetworksData[0].chainId}`,
);
const selectedNetwork = retrievedNetworksData.find(
(network) => network.chainId === newNetworkData.chainId,
);
await addAccountsForNetwork(selectedNetwork!, nextAccountId - 1);
return retrievedNetworksData;
}
const storeNetworkData = async (
networkData: NetworksFormData,
): Promise<NetworksDataState[]> => {
@@ -180,21 +251,25 @@ const storeNetworkData = async (
networkId: String(networkId),
},
];
await setInternetCredentials(
'networks',
'_',
JSON.stringify(updatedNetworks),
);
return updatedNetworks;
};
const retrieveNetworksData = async (): Promise<NetworksDataState[]> => {
const networks = await getInternetCredentials('networks');
if(!networks){
if (!networks) {
return [];
}
const parsedNetworks: NetworksDataState[] = JSON.parse(networks);
return parsedNetworks;
};
@@ -217,6 +292,7 @@ export const retrieveAccountsForNetwork = async (
address,
hdPath: path,
};
return account;
}),
);
@@ -234,6 +310,7 @@ const retrieveAccounts = async (
if (!accountIndices) {
return;
}
const loadedAccounts = await retrieveAccountsForNetwork(
`${currentNetworkData.namespace}:${currentNetworkData.chainId}`,
accountIndices,
@@ -267,7 +344,7 @@ const retrieveSingleAccount = async (
throw new Error('Accounts for given chain not found');
}
return loadedAccounts.find(account => account.address === address);
return loadedAccounts.find(account => account.address.toLowerCase() === address.toLowerCase());
};
const resetWallet = async () => {
@@ -368,6 +445,28 @@ const getCosmosAccountByHDPath = async (
return { cosmosWallet, data };
};
const checkNetworkForChainID = async (
chainId: string,
): Promise<boolean> => {
const networks = await getInternetCredentials('networks');
if (!networks) {
return false;
}
const networksData: NetworksFormData[] = JSON.parse(networks);
return networksData.some((network) => network.chainId === chainId);
}
const isWalletCreated = async (
): Promise<boolean> => {
const mnemonicServer = await getInternetCredentials("mnemonicServer");
const mnemonic = mnemonicServer;
return mnemonic !== null;
};
export {
createWallet,
addAccount,
@@ -382,4 +481,7 @@ export {
getNextAccountId,
updateAccountCounter,
getCosmosAccountByHDPath,
addNewNetwork,
checkNetworkForChainID,
isWalletCreated
};
+35 -27
View File
@@ -18,30 +18,6 @@ export const DEFAULT_NETWORKS: NetworksFormData[] = [
gasPrice: '0.001',
isDefault: true,
},
{
chainId: 'zenith-testnet-stage0',
networkName: 'zenithd stage 0 testnet',
namespace: COSMOS,
rpcUrl: import.meta.env.REACT_APP_ZENITHD_RPC_URL || '',
blockExplorerUrl: '',
nativeDenom: 'znt',
addressPrefix: 'zenith',
coinType: '118',
gasPrice: '0.01',
isDefault: true,
},
{
chainId: 'zenith-testnet-stage1',
networkName: 'zenithd stage 1 testnet',
namespace: COSMOS,
rpcUrl: import.meta.env.REACT_APP_ZENITHD_RPC_URL || '',
blockExplorerUrl: '',
nativeDenom: 'znt',
addressPrefix: 'zenith',
coinType: '118',
gasPrice: '0.01',
isDefault: true,
},
{
chainId: 'laconic_9000-1',
networkName: 'laconicd',
@@ -64,11 +40,23 @@ export const DEFAULT_NETWORKS: NetworksFormData[] = [
coinType: '60',
isDefault: true,
},
// Base Chain Network
{
chainId: 'theta-testnet-001',
networkName: COSMOS_TESTNET_CHAINS['cosmos:theta-testnet-001'].name,
chainId: '8453',
networkName: EIP155_CHAINS['eip155:8453'].name,
namespace: EIP155,
rpcUrl: EIP155_CHAINS['eip155:8453'].rpc,
blockExplorerUrl: '',
currencySymbol: 'ETH',
coinType: '60',
isDefault: true,
},
{
chainId: 'provider',
networkName: COSMOS_TESTNET_CHAINS['cosmos:provider'].name,
namespace: COSMOS,
rpcUrl: COSMOS_TESTNET_CHAINS['cosmos:theta-testnet-001'].rpc,
rpcUrl: COSMOS_TESTNET_CHAINS['cosmos:provider'].rpc,
blockExplorerUrl: '',
nativeDenom: 'uatom',
addressPrefix: 'cosmos',
@@ -76,6 +64,22 @@ export const DEFAULT_NETWORKS: NetworksFormData[] = [
gasPrice: '0.025',
isDefault: true,
},
//TODO: Add network from android app
{
chainId: 'nyx',
networkName: 'Nym',
namespace: COSMOS,
rpcUrl: 'https://rpc.nymtech.net',
blockExplorerUrl: 'https://explorer.nymtech.net',
nativeDenom: 'unym',
addressPrefix: 'n',
coinType: '118',
// Ref: https://nym.com/docs/operators/nodes/validator-setup#apptoml-configuration
gasPrice: '0.025',
isDefault: true,
},
];
export const CHAINID_DEBOUNCE_DELAY = 250;
@@ -98,6 +102,7 @@ export const REQUEST_ACCOUNT_PK = 'REQUEST_ACCOUNT_PK';
export const REQUEST_ADD_ACCOUNT = 'REQUEST_ADD_ACCOUNT';
export const AUTO_SIGN_IN = 'AUTO_SIGN_IN';
export const CHECK_BALANCE = 'CHECK_BALANCE';
export const REQUEST_ADD_NETWORK = 'REQUEST_ADD_NETWORK';
// iframe response types
export const COSMOS_ACCOUNTS_RESPONSE = 'COSMOS_ACCOUNTS_RESPONSE';
@@ -109,3 +114,6 @@ export const ACCOUNT_PK_RESPONSE = 'ACCOUNT_PK_RESPONSE';
export const ADD_ACCOUNT_RESPONSE = 'ADD_ACCOUNT_RESPONSE';
export const WALLET_ACCOUNTS_DATA = 'WALLET_ACCOUNTS_DATA';
export const IS_SUFFICIENT = 'IS_SUFFICIENT';
export const NETWORK_ADDED_RESPONSE = "NETWORK_ADDED_RESPONSE";
export const NETWORK_ALREADY_EXISTS_RESPONSE = "NETWORK_ALREADY_EXISTS_RESPONSE";
export const NETWORK_ADD_FAILED_RESPONSE = "NETWORK_ADD_FAILED_RESPONSE";
+3 -3
View File
@@ -19,10 +19,10 @@ export const COSMOS_TESTNET_CHAINS: Record<
namespace: string;
}
> = {
'cosmos:theta-testnet-001': {
chainId: 'theta-testnet-001',
'cosmos:provider': {
chainId: 'provider',
name: 'Cosmos Hub Testnet',
rpc: 'https://rpc-t.cosmos.nodestake.top',
rpc: 'https://rpc-rs.cosmos.nodestake.top',
namespace: 'cosmos',
},
};
+7 -9
View File
@@ -11,13 +11,8 @@
export type TEIP155Chain = keyof typeof EIP155_CHAINS;
export type EIP155Chain = {
chainId: number;
name: string;
logo: string;
rgb: string;
rpc: string;
namespace: string;
smartAccountEnabled?: boolean;
};
/**
@@ -25,12 +20,14 @@ export type EIP155Chain = {
*/
export const EIP155_CHAINS: Record<string, EIP155Chain> = {
'eip155:1': {
chainId: 1,
name: 'Ethereum',
logo: '/chain-logos/eip155-1.png',
rgb: '99, 125, 234',
rpc: 'https://cloudflare-eth.com/',
namespace: 'eip155',
},
// Ref: https://docs.base.org/base-chain/quickstart/connecting-to-base#base-mainnet
'eip155:8453': {
name: 'Base',
rpc: 'https://mainnet.base.org',
},
};
@@ -40,4 +37,5 @@ export const EIP155_CHAINS: Record<string, EIP155Chain> = {
export const EIP155_SIGNING_METHODS = {
PERSONAL_SIGN: 'personal_sign',
ETH_SEND_TRANSACTION: 'eth_sendTransaction',
WALLET_GET_CAPABILITIES: 'wallet_getCapabilities'
};
@@ -10,7 +10,6 @@ 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_ZENITHD_RPC_URL: ${CERC_ZENITHD_RPC_URL}"
echo "CERC_ALLOWED_URLS: ${CERC_ALLOWED_URLS}"
# Build with required env
@@ -18,7 +17,6 @@ export REACT_APP_WALLET_CONNECT_PROJECT_ID=$WALLET_CONNECT_ID
export REACT_APP_DEFAULT_GAS_PRICE=$CERC_DEFAULT_GAS_PRICE
export REACT_APP_GAS_ADJUSTMENT=$CERC_GAS_ADJUSTMENT
export REACT_APP_LACONICD_RPC_URL=$CERC_LACONICD_RPC_URL
export REACT_APP_ZENITHD_RPC_URL=$CERC_ZENITHD_RPC_URL
export REACT_APP_ALLOWED_URLS=$CERC_ALLOWED_URLS
# Set env variables in build