Add new component WalletEmbed to handle tx requests via iframe messaging #18

Merged
nabarun merged 16 commits from deep-stack/laconic-wallet-web:ag-iframe into main 2024-11-12 09:46:17 +00:00
Showing only changes of commit d95483310f - Show all commits

View File

@ -5,6 +5,7 @@ import {
Text, Text,
TextInput, TextInput,
} from 'react-native-paper'; } from 'react-native-paper';
import { BigNumber } from 'ethers';
import { DirectSecp256k1Wallet } from '@cosmjs/proto-signing'; import { DirectSecp256k1Wallet } from '@cosmjs/proto-signing';
import { import {
@ -13,22 +14,31 @@ import {
SigningStargateClient, SigningStargateClient,
} from '@cosmjs/stargate'; } from '@cosmjs/stargate';
import { createWallet, retrieveAccounts } from '../utils/accounts'; import { createWallet, retrieveAccounts, retrieveSingleAccount } from '../utils/accounts';
import AccountDetails from '../components/AccountDetails'; import AccountDetails from '../components/AccountDetails';
import styles from '../styles/stylesheet'; import styles from '../styles/stylesheet';
import { retrieveSingleAccount } from '../utils/accounts';
import DataBox from '../components/DataBox'; import DataBox from '../components/DataBox';
import { getPathKey } from '../utils/misc'; import { getPathKey } from '../utils/misc';
import { useNetworks } from '../context/NetworksContext'; import { useNetworks } from '../context/NetworksContext';
import TxErrorDialog from '../components/TxErrorDialog'; import TxErrorDialog from '../components/TxErrorDialog';
import { BigNumber } from 'ethers';
import { MEMO } from '../screens/ApproveTransfer'; import { MEMO } from '../screens/ApproveTransfer';
import { Account, NetworksDataState } from '../types';
type TransactionDetails = {
chainId: string;
fromAddress: string;
toAddress: string;
amount: string;
account: Account
balance: string;
requestedNetwork: NetworksDataState
};
export const WalletEmbed = () => { export const WalletEmbed = () => {
const [isTxRequested, setIsTxRequested] = useState<boolean>(false); const [isTxRequested, setIsTxRequested] = useState<boolean>(false);
const [transactionDetails, setTransactionDetails] = useState<any>(null); const [transactionDetails, setTransactionDetails] = useState<TransactionDetails | null>(null);
const [fees, setFees] = useState(''); const [fees, setFees] = useState<string>('');
const [gasLimit, setGasLimit] = useState(''); const [gasLimit, setGasLimit] = useState<string>('');
const [isTxLoading, setIsTxLoading] = useState(false); const [isTxLoading, setIsTxLoading] = useState(false);
const [txError, setTxError] = useState<string | null>(null); const [txError, setTxError] = useState<string | null>(null);
const txEventRef = useRef<MessageEvent | null>(null); const txEventRef = useRef<MessageEvent | null>(null);
@ -36,7 +46,7 @@ export const WalletEmbed = () => {
const { networksData } = useNetworks(); const { networksData } = useNetworks();
const getAccountsData = useCallback(async (chainId: string) => { const getAccountsData = useCallback(async (chainId: string) => {
const targetNetwork = networksData.find(network => network.chainId === "laconic-testnet-2"); const targetNetwork = networksData.find(network => network.chainId === chainId);
if (!targetNetwork) { if (!targetNetwork) {
return ''; return '';
@ -53,11 +63,8 @@ export const WalletEmbed = () => {
}, [networksData]); }, [networksData]);
const getAddressesFromData = (accountsData: string): string[] => { const getAddressesFromData = (accountsData: string): string[] =>
return accountsData accountsData?.split(',') || [];
? accountsData.split(',')
: [];
};
const sendMessage = ( const sendMessage = (
source: Window | null, source: Window | null,
@ -114,81 +121,81 @@ export const WalletEmbed = () => {
}; };
}, [networksData, getAccountsData]); }, [networksData, getAccountsData]);
const handleTxRequested = async (event: MessageEvent) => { const handleTxRequested = useCallback(
if (event.data.type !== 'REQUEST_TX') return; async (event: MessageEvent) => {
try {
if (event.data.type !== 'REQUEST_TX') return;
txEventRef.current = event; txEventRef.current = event;
const { chainId, fromAddress, toAddress, amount } = event.data; const { chainId, fromAddress, toAddress, amount } = event.data;
const network = networksData.find( const network = networksData.find(net => net.chainId === chainId);
net => net.chainId === chainId
);
if (!network) { if (!network) {
console.error('Network not found'); console.error('Network not found');
return; throw new Error('Requested network not supported.');
} }
const account = await retrieveSingleAccount(network.namespace, network.chainId, fromAddress); const account = await retrieveSingleAccount(network.namespace, network.chainId, fromAddress);
if (!account) { if (!account) {
throw new Error('Account not found'); throw new Error('Account not found for the requested address.');
} }
const cosmosPrivKey = ( const cosmosPrivKey = (
await getPathKey(`${network.namespace}:${chainId}`, account.index) await getPathKey(`${network.namespace}:${chainId}`, account.index)
).privKey; ).privKey;
const sender = await DirectSecp256k1Wallet.fromKey(
Buffer.from(cosmosPrivKey.split('0x')[1], 'hex'),
network.addressPrefix
);
const client = await SigningStargateClient.connectWithSigner( const sender = await DirectSecp256k1Wallet.fromKey(
network.rpcUrl!, Buffer.from(cosmosPrivKey.split('0x')[1], 'hex'),
sender network.addressPrefix
); );
const balance = await client?.getBalance( const client = await SigningStargateClient.connectWithSigner(network.rpcUrl!, sender);
account.address,
network!.nativeDenom!.toLowerCase(),
);
const sendMsg = { const balance = await client.getBalance(
typeUrl: '/cosmos.bank.v1beta1.MsgSend', account.address,
value: { network.nativeDenom!.toLowerCase()
fromAddress: fromAddress, );
toAddress: toAddress,
amount: [ const sendMsg = {
{ typeUrl: '/cosmos.bank.v1beta1.MsgSend',
amount: String(amount), value: {
denom: network.nativeDenom!, fromAddress: fromAddress,
toAddress: toAddress,
amount: [
{
amount: String(amount),
denom: network.nativeDenom!,
},
],
}, },
], };
},
};
const gasEstimation = await client.simulate(fromAddress, [sendMsg], MEMO); const gasEstimation = await client.simulate(fromAddress, [sendMsg], MEMO);
const gasLimit = const gasLimit = String(
String( Math.round(gasEstimation * Number(process.env.REACT_APP_GAS_ADJUSTMENT))
Math.round(gasEstimation * Number(process.env.REACT_APP_GAS_ADJUSTMENT)), );
) setGasLimit(gasLimit);
setGasLimit(gasLimit);
const gasPrice = GasPrice.fromString(network.gasPrice! + network.nativeDenom);
const cosmosFees = calculateFee(Number(gasLimit), gasPrice);
setFees(cosmosFees.amount[0].amount);
setTransactionDetails({ const gasPrice = GasPrice.fromString(`${network.gasPrice}${network.nativeDenom}`);
chainId, const cosmosFees = calculateFee(Number(gasLimit), gasPrice);
fromAddress, setFees(cosmosFees.amount[0].amount);
toAddress,
amount,
account,
balance: balance.amount,
requestedNetwork: network,
});
setIsTxRequested(true); setTransactionDetails({
}; chainId,
fromAddress,
toAddress,
amount,
account,
balance: balance.amount,
requestedNetwork: network,
});
setIsTxRequested(true);
} catch (error) {
console.error('Error processing transaction request:', error);
}
}, [networksData]);
useEffect(() => { useEffect(() => {
window.addEventListener('message', handleTxRequested); window.addEventListener('message', handleTxRequested);
@ -198,44 +205,44 @@ export const WalletEmbed = () => {
const acceptRequestHandler = async () => { const acceptRequestHandler = async () => {
try { try {
setIsTxLoading(true); setIsTxLoading(true);
const { chainId, fromAddress, toAddress, amount, requestedNetwork } = if (!transactionDetails) {
transactionDetails; throw new Error('Tx details not set');
}
const balanceBigNum = BigNumber.from(transactionDetails.balance); const balanceBigNum = BigNumber.from(transactionDetails.balance);
const amountBigNum = BigNumber.from(String(amount)); const amountBigNum = BigNumber.from(String(transactionDetails.amount));
if (amountBigNum.gte(balanceBigNum)) { if (amountBigNum.gte(balanceBigNum)) {
throw new Error('Insufficient funds'); throw new Error('Insufficient funds');
} }
const cosmosPrivKey = ( const cosmosPrivKey = (
await getPathKey(`${requestedNetwork.namespace}:${chainId}`, transactionDetails.account.index) await getPathKey(`${transactionDetails.requestedNetwork.namespace}:${transactionDetails.chainId}`, transactionDetails.account.index)
).privKey; ).privKey;
const sender = await DirectSecp256k1Wallet.fromKey( const sender = await DirectSecp256k1Wallet.fromKey(
Buffer.from(cosmosPrivKey.split('0x')[1], 'hex'), Buffer.from(cosmosPrivKey.split('0x')[1], 'hex'),
requestedNetwork.addressPrefix transactionDetails.requestedNetwork.addressPrefix
); );
const client = await SigningStargateClient.connectWithSigner( const client = await SigningStargateClient.connectWithSigner(
requestedNetwork.rpcUrl!, transactionDetails.requestedNetwork.rpcUrl!,
sender sender
); );
const fee = calculateFee( const fee = calculateFee(
Number(gasLimit), Number(gasLimit),
GasPrice.fromString(`${requestedNetwork.gasPrice}${requestedNetwork.nativeDenom}`) GasPrice.fromString(`${transactionDetails.requestedNetwork.gasPrice}${transactionDetails.requestedNetwork.nativeDenom}`)
); );
const txResult = await client.sendTokens( const txResult = await client.sendTokens(
fromAddress, transactionDetails.fromAddress,
toAddress, transactionDetails.toAddress,
[{ amount: String(amount), denom: requestedNetwork.nativeDenom }], [{ amount: String(transactionDetails.amount), denom: transactionDetails.requestedNetwork.nativeDenom! }],
fee fee
); );
const event = txEventRef.current; const event = txEventRef.current;
if (event?.source) { if (event?.source) {
sendMessage(event.source as Window, 'TRANSACTION_SUCCESS', txResult.transactionHash, '*'); sendMessage(event.source as Window, 'TRANSACTION_SUCCESS', txResult.transactionHash, event.origin);
} else { } else {
console.error('No event source available to send message'); console.error('No event source available to send message');
} }