diff --git a/src/screens/ApproveTransfer.tsx b/src/screens/ApproveTransfer.tsx index 80f88be..27a912e 100644 --- a/src/screens/ApproveTransfer.tsx +++ b/src/screens/ApproveTransfer.tsx @@ -46,24 +46,29 @@ 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 & { + route: { + params: { + transaction: any; + requestEvent?: { + params: { + chainId: string; + request: { + method: string; + }; + }; + }; + requestSessionData?: any; + chainId?: string; + }; + path?: string; + }; +}; -const ApproveTransfer = ({ route }: SignRequestProps) => { +const ApproveTransfer = ({ route }: ApproveTransferProps) => { const { networksData } = useNetworks(); const { web3wallet } = useWalletConnect(); - 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 transaction = route.params.transaction; - const requestEvent = route.params.requestEvent; - const chainId = requestEvent.params.chainId; - const requestMethod = requestEvent.params.request.method; - const [account, setAccount] = useState(); const [isLoading, setIsLoading] = useState(true); const [balance, setBalance] = useState(''); @@ -80,6 +85,80 @@ const ApproveTransfer = ({ route }: SignRequestProps) => { const [ethMaxPriorityFee, setEthMaxPriorityFee] = useState(); + const navigation = + useNavigation>(); + + // 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 transaction = route.params?.transaction; + const requestEvent = route.params?.requestEvent; + const chainId = requestEvent?.params?.chainId || route.params?.chainId; + const requestMethod = requestEvent?.params?.request?.method; + + const sanitizePath = useCallback((path: string) => { + const regex = /^\/transfer\/(eip155|cosmos)\/(.+)\/(.+)\/(.+)\/(.+)$/; + const match = path.match(regex); + if (match) { + const [, pathNamespace, pathChainId, pathAddress, pathTo, pathAmount] = match; + return { + namespace: pathNamespace, + chainId: pathChainId, + address: pathAddress, + to: pathTo, + amount: pathAmount, + }; + } else { + navigation.navigate('InvalidPath'); + } + return null; + }, [navigation]); + + const retrieveData = useCallback(async (requestNamespace: string, requestChainId: string, requestAddress: string) => { + const requestAccount = await retrieveSingleAccount( + requestNamespace, + requestChainId, + requestAddress, + ); + if (!requestAccount) { + navigation.navigate('InvalidPath'); + return; + } + + setAccount(requestAccount); + }, [navigation]); + + useEffect(() => { + if (route.path) { + const sanitizedRoute = sanitizePath(route.path); + if (sanitizedRoute) { + retrieveData( + sanitizedRoute.namespace, + sanitizedRoute.chainId, + sanitizedRoute.address, + ); + return; + } + } + + if (requestEvent) { + const requestedNetwork = networksData.find( + networkData => { + return `${networkData.namespace}:${networkData.chainId}` === chainId; + } + ); + if (requestedNetwork && transaction?.from) { + retrieveData( + requestedNetwork.namespace, + requestedNetwork.chainId, + transaction.from, + ); + } + } + }, [retrieveData, sanitizePath, route, networksData, requestEvent, chainId, transaction]); + const isSufficientFunds = useMemo(() => { if (!transaction.value) { return; @@ -139,7 +218,7 @@ const ApproveTransfer = ({ route }: SignRequestProps) => { ).privKey; const sender = await DirectSecp256k1Wallet.fromKey( - Buffer.from(cosmosPrivKey.split('0x')[1], 'hex'), + Uint8Array.from(Buffer.from(cosmosPrivKey.split('0x')[1], 'hex')), requestedNetwork?.addressPrefix, ); @@ -185,26 +264,6 @@ const ApproveTransfer = ({ route }: SignRequestProps) => { } }, [requestedNetwork, namespace]); - const navigation = - useNavigation>(); - - const retrieveData = useCallback( - async (requestAddress: string) => { - const requestAccount = await retrieveSingleAccount( - requestedNetwork!.namespace, - requestedNetwork!.chainId, - requestAddress, - ); - if (!requestAccount) { - navigation.navigate('InvalidPath'); - return; - } - - setAccount(requestAccount); - }, - [navigation, requestedNetwork], - ); - 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 @@ -246,9 +305,6 @@ const ApproveTransfer = ({ route }: SignRequestProps) => { requestedNetwork, ethMaxFee, ]); - useEffect(() => { - retrieveData(transaction.from!); - }, [retrieveData, transaction]); const isEIP1559 = useMemo(() => { if (cosmosGasLimit) { @@ -260,6 +316,101 @@ const ApproveTransfer = ({ route }: SignRequestProps) => { return false; }, [cosmosGasLimit, ethMaxFee, ethMaxPriorityFee]); + const handleIntent = async () => { + if (!account) { + throw new Error('Account is not valid'); + } + + if (route.path) { + const sanitizedRoute = sanitizePath(route.path); + if (!sanitizedRoute) { + throw new Error('Invalid path'); + } + + const requestedNetwork = networksData.find( + networkData => networkData.chainId === sanitizedRoute.chainId, + ); + + if (!requestedNetwork) { + throw new Error('Network not found'); + } + + const cosmosPrivKey = ( + await getPathKey( + `${requestedNetwork.namespace}:${requestedNetwork.chainId}`, + account.index, + ) + ).privKey; + + const sender = await DirectSecp256k1Wallet.fromKey( + Uint8Array.from(Buffer.from(cosmosPrivKey.split('0x')[1], 'hex')), + requestedNetwork.addressPrefix, + ); + + const client = await SigningStargateClient.connectWithSigner( + requestedNetwork.rpcUrl!, + sender, + ); + + const sendMsg: MsgSendEncodeObject = { + typeUrl: '/cosmos.bank.v1beta1.MsgSend', + value: { + fromAddress: account.address, + toAddress: sanitizedRoute.to, + amount: [ + { + amount: String(sanitizedRoute.amount), + denom: requestedNetwork.nativeDenom!, + }, + ], + }, + }; + + const gasEstimation = await client.simulate( + account.address, + [sendMsg], + MEMO, + ); + + const gasLimit = String( + Math.round(gasEstimation * Number(process.env.REACT_APP_GAS_ADJUSTMENT)), + ); + + const gasPrice = GasPrice.fromString( + requestedNetwork.gasPrice! + requestedNetwork.nativeDenom, + ); + + const cosmosFees = calculateFee(Number(gasLimit), gasPrice); + + const result = await client.signAndBroadcast( + account.address, + [sendMsg], + { + amount: [ + { + amount: cosmosFees.amount[0].amount, + denom: requestedNetwork.nativeDenom!, + }, + ], + gas: gasLimit, + }, + MEMO, + ); + + // Convert BigInt values to strings before sending to Android + const serializedResult = JSON.stringify(result, (key, value) => + typeof value === 'bigint' ? value.toString() : value + ); + + // Send the result back to Android and close dialog + if (window.Android?.onTransferComplete) { + window.Android.onTransferComplete(serializedResult); + } else { + alert(`Transaction: ${serializedResult}`); + } + } + }; + const acceptRequestHandler = async () => { setIsTxLoading(true); try { @@ -267,77 +418,80 @@ 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: 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'); + } else { + // Handle direct intent + await handleIntent(); + navigation.navigate('Home'); } - - 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 (!(error instanceof Error)) { throw error; @@ -350,20 +504,26 @@ const ApproveTransfer = ({ route }: SignRequestProps) => { }; const rejectRequestHandler = async () => { - const response = rejectWalletConnectRequest(requestEvent); - const { topic } = requestEvent; - await web3wallet!.respondSessionRequest({ - topic, - response, - }); + 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'); + } }; useEffect(() => { const getAccountBalance = async () => { try { - if (!account) { + if (!account || !requestedNetwork) { return; } if (namespace === EIP155) { @@ -373,20 +533,20 @@ const ApproveTransfer = ({ route }: SignRequestProps) => { const fetchedBalance = await provider.getBalance(account.address); setBalance(fetchedBalance ? fetchedBalance.toString() : '0'); } else { - const cosmosBalance = await cosmosStargateClient?.getBalance( + if (!cosmosStargateClient) { + return; + } + const cosmosBalance = await cosmosStargateClient.getBalance( account.address, - requestedNetwork!.nativeDenom!.toLowerCase(), + requestedNetwork.nativeDenom!.toLowerCase(), ); - - setBalance(cosmosBalance?.amount!); + setBalance(cosmosBalance?.amount || '0'); } } catch (error) { - if (!(error instanceof Error)) { - throw error; - } - - setTxError(error.message); - setIsTxErrorDialogOpen(true); + console.error('Error fetching balance:', error); + setBalance('0'); + // Don't show error dialog for balance fetch failures + // Just set balance to 0 and let the transaction proceed } }; @@ -508,16 +668,18 @@ const ApproveTransfer = ({ route }: SignRequestProps) => { ) : ( <> - - {requestIcon && ( - - )} - {requestName} - {requestURL} - + {requestSession && ( + + {requestIcon && ( + + )} + {requestName} + {requestURL} + + )} From @@ -528,11 +690,7 @@ const ApproveTransfer = ({ route }: SignRequestProps) => { label={`Balance (${ namespace === EIP155 ? 'wei' : requestedNetwork!.nativeDenom })`} - data={ - balance === '' || balance === undefined - ? 'Loading balance...' - : `${balance}` - } + data={balance || '0'} /> {transaction && ( diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 8d03e44..10525c4 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -41,10 +41,10 @@ export const DEFAULT_NETWORKS: NetworksFormData[] = [ isDefault: true, }, { - chainId: 'theta-testnet-001', - networkName: COSMOS_TESTNET_CHAINS['cosmos:theta-testnet-001'].name, + 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', diff --git a/src/utils/wallet-connect/COSMOSData.ts b/src/utils/wallet-connect/COSMOSData.ts index a1c6baf..2a9c50d 100644 --- a/src/utils/wallet-connect/COSMOSData.ts +++ b/src/utils/wallet-connect/COSMOSData.ts @@ -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', }, };