Compare commits
5 Commits
main
...
nym-vpn-ap
| Author | SHA1 | Date | |
|---|---|---|---|
| d8d100535e | |||
| 2feec2db18 | |||
| 86deb2aab7 | |||
| 96c0a4df06 | |||
| 1e88321490 |
@ -1,9 +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-mainnet-1.laconic.com
|
||||
REACT_APP_LACONICD_RPC_URL=https://laconicd-sapo.laconic.com
|
||||
|
||||
# Example: https://example-url-1.com,https://example-url-2.com
|
||||
REACT_APP_ALLOWED_URLS=
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web-wallet",
|
||||
"version": "0.1.8",
|
||||
"version": "0.1.7-nym-0.1.3",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@cerc-io/registry-sdk": "^0.2.5",
|
||||
|
||||
18
src/App.tsx
18
src/App.tsx
@ -146,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 = {
|
||||
@ -347,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");
|
||||
}}
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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>
|
||||
|
||||
33
src/global.d.ts
vendored
33
src/global.d.ts
vendored
@ -3,6 +3,15 @@ declare global {
|
||||
interface Window {
|
||||
// Android bridge callbacks for signature and accounts related events
|
||||
Android?: {
|
||||
// Store a key-value pair securely in encrypted storage
|
||||
setItem(key: string, value: string): boolean;
|
||||
|
||||
// Retrieve a value by key from encrypted storage
|
||||
getItem(key: string): string | null;
|
||||
|
||||
// Remove a key-value pair from encrypted storage
|
||||
removeItem(key: string): boolean;
|
||||
|
||||
// Called when signature is successfully generated
|
||||
onSignatureComplete?: (signature: string) => void;
|
||||
|
||||
@ -14,10 +23,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -58,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;
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}>
|
||||
|
||||
@ -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
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -344,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 () => {
|
||||
|
||||
@ -6,28 +6,28 @@ export const EIP155 = 'eip155';
|
||||
export const COSMOS = 'cosmos';
|
||||
|
||||
export const DEFAULT_NETWORKS: NetworksFormData[] = [
|
||||
{
|
||||
chainId: 'laconic-mainnet',
|
||||
networkName: 'laconicd mainnet',
|
||||
namespace: COSMOS,
|
||||
rpcUrl: import.meta.env.REACT_APP_LACONICD_RPC_URL,
|
||||
blockExplorerUrl: 'https://explorer.laconic.com/laconic-mainnet',
|
||||
nativeDenom: 'alnt',
|
||||
addressPrefix: 'laconic',
|
||||
coinType: '118',
|
||||
gasPrice: '0.001',
|
||||
isDefault: false,
|
||||
},
|
||||
{
|
||||
chainId: 'laconic-testnet-2',
|
||||
networkName: 'laconicd testnet-2',
|
||||
namespace: COSMOS,
|
||||
rpcUrl: 'https://laconicd-sapo.laconic.com',
|
||||
rpcUrl: import.meta.env.REACT_APP_LACONICD_RPC_URL!,
|
||||
blockExplorerUrl: '',
|
||||
nativeDenom: 'alnt',
|
||||
addressPrefix: 'laconic',
|
||||
coinType: '118',
|
||||
gasPrice: '0.001',
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
chainId: 'laconic_9000-1',
|
||||
networkName: 'laconicd',
|
||||
namespace: COSMOS,
|
||||
rpcUrl: "https://laconicd.laconic.com",
|
||||
blockExplorerUrl: '',
|
||||
nativeDenom: 'alnt',
|
||||
addressPrefix: 'laconic',
|
||||
coinType: '118',
|
||||
gasPrice: '1',
|
||||
isDefault: false,
|
||||
},
|
||||
{
|
||||
@ -40,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',
|
||||
@ -52,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;
|
||||
|
||||
@ -1,13 +1,28 @@
|
||||
const setInternetCredentials = (name:string, username:string, password:string) => {
|
||||
localStorage.setItem(name, password);
|
||||
if (window.Android?.setItem) {
|
||||
window.Android.setItem(name, password);
|
||||
} else {
|
||||
localStorage.setItem(name, password);
|
||||
}
|
||||
};
|
||||
|
||||
const getInternetCredentials = (name:string) : string | null => {
|
||||
return localStorage.getItem(name);
|
||||
if (window.Android?.getItem) {
|
||||
const result = window.Android.getItem(name);
|
||||
|
||||
// Normalize undefined to null to match localStorage behavior
|
||||
return result === undefined ? null : result;
|
||||
} else {
|
||||
return localStorage.getItem(name);
|
||||
}
|
||||
};
|
||||
|
||||
const resetInternetCredentials = (name:string) => {
|
||||
localStorage.removeItem(name);
|
||||
if (window.Android?.removeItem) {
|
||||
window.Android.removeItem(name);
|
||||
} else {
|
||||
localStorage.removeItem(name);
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
|
||||
@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
@ -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,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_ZENITHD_RPC_URL: ${CERC_ZENITHD_RPC_URL}
|
||||
CERC_ALLOWED_URLS: ${CERC_ALLOWED_URLS}
|
||||
command: ["bash", "/scripts/run.sh"]
|
||||
volumes:
|
||||
|
||||
@ -65,7 +65,10 @@ Instructions for running the `laconic-wallet-web` using [laconic-so](https://git
|
||||
CERC_GAS_ADJUSTMENT=
|
||||
|
||||
# RPC endpoint of laconicd node (default: https://laconicd.laconic.com)
|
||||
CERC_LACONICD_RPC_URL=https://laconicd-mainnet-1.laconic.com
|
||||
CERC_LACONICD_RPC_URL=
|
||||
|
||||
# Zenith RPC endpoint
|
||||
CERC_ZENITHD_RPC_URL=
|
||||
```
|
||||
|
||||
## Start the deployment
|
||||
|
||||
Loading…
Reference in New Issue
Block a user