Compare commits

..

3 Commits

Author SHA1 Message Date
b527a9486d Add new component WalletEmbed to handle tx requests via iframe messaging (#18)
Part of [Service provider auctions for web deployments](https://www.notion.so/Service-provider-auctions-for-web-deployments-104a6b22d47280dbad51d28aa3a91d75)

Co-authored-by: Adw8 <adwaitgharpure@gmail.com>
Co-authored-by: Isha <ishavenikar7@gmail.com>
Reviewed-on: cerc-io/laconic-wallet-web#18
Co-authored-by: Nabarun <nabarun@deepstacksoft.com>
Co-committed-by: Nabarun <nabarun@deepstacksoft.com>
2024-11-12 09:46:16 +00:00
b94fd22c76 Add laconicd testnet-2 as default network (#17)
Part of [Service provider auctions for web deployments](https://www.notion.so/Service-provider-auctions-for-web-deployments-104a6b22d47280dbad51d28aa3a91d75)

Co-authored-by: Shreerang Kale <shreerangkale@gmail.com>
Reviewed-on: cerc-io/laconic-wallet-web#17
2024-10-28 12:13:13 +00:00
657c39e5ed Add config for wallet connect verify code (#16)
Part of [Service provider auctions for web deployments](https://www.notion.so/Service-provider-auctions-for-web-deployments-104a6b22d47280dbad51d28aa3a91d75)

Reviewed-on: cerc-io/laconic-wallet-web#16
Co-authored-by: Nabarun <nabarun@deepstacksoft.com>
Co-committed-by: Nabarun <nabarun@deepstacksoft.com>
2024-10-25 13:50:11 +00:00
11 changed files with 403 additions and 67 deletions

View File

@ -2,4 +2,4 @@ REACT_APP_WALLET_CONNECT_PROJECT_ID=
REACT_APP_DEFAULT_GAS_PRICE=0.025 REACT_APP_DEFAULT_GAS_PRICE=0.025
# Reference: https://github.com/cosmos/cosmos-sdk/issues/16020 # Reference: https://github.com/cosmos/cosmos-sdk/issues/16020
REACT_APP_GAS_ADJUSTMENT=2 REACT_APP_GAS_ADJUSTMENT=2
REACT_APP_LACONICD_RPC_URL=https://laconicd.laconic.com REACT_APP_LACONICD_RPC_URL=https://laconicd-sapo.laconic.com

View File

@ -34,6 +34,7 @@ import { NETWORK_METHODS } from "./utils/wallet-connect/common-data";
import { COSMOS_METHODS } from "./utils/wallet-connect/COSMOSData"; import { COSMOS_METHODS } from "./utils/wallet-connect/COSMOSData";
import styles from "./styles/stylesheet"; import styles from "./styles/stylesheet";
import { Header } from "./components/Header"; import { Header } from "./components/Header";
import { WalletEmbed } from "./screens/WalletEmbed";
const Stack = createStackNavigator<StackParamsList>(); const Stack = createStackNavigator<StackParamsList>();
@ -313,6 +314,13 @@ const App = (): React.JSX.Element => {
header: () => <Header title="Wallet" />, header: () => <Header title="Wallet" />,
}} }}
/> />
<Stack.Screen
name="WalletEmbed"
component={WalletEmbed}
options={{
header: () => <></>,
}}
/>
</Stack.Navigator> </Stack.Navigator>
<PairingModal <PairingModal
visible={modalVisible} visible={modalVisible}

View File

@ -22,7 +22,6 @@ import {
EMPTY_FIELD_ERROR, EMPTY_FIELD_ERROR,
INVALID_URL_ERROR, INVALID_URL_ERROR,
IS_NUMBER_REGEX, IS_NUMBER_REGEX,
LACONIC,
} from "../utils/constants"; } from "../utils/constants";
import { getCosmosAccounts } from "../utils/accounts"; import { getCosmosAccounts } from "../utils/accounts";
import ETH_CHAINS from "../assets/ethereum-chains.json"; import ETH_CHAINS from "../assets/ethereum-chains.json";
@ -163,7 +162,6 @@ const AddNetwork = () => {
break; break;
case COSMOS: case COSMOS:
case LACONIC:
address = ( address = (
await getCosmosAccounts( await getCosmosAccounts(
mnemonic, mnemonic,

362
src/screens/WalletEmbed.tsx Normal file
View File

@ -0,0 +1,362 @@
import React, { useEffect, useState, useCallback, useRef } from 'react';
import { ScrollView, View } from 'react-native';
import {
ActivityIndicator,
Button,
Text,
TextInput,
} from 'react-native-paper';
import { BigNumber } from 'ethers';
import { DirectSecp256k1Wallet } from '@cosmjs/proto-signing';
import {
calculateFee,
GasPrice,
SigningStargateClient,
} from '@cosmjs/stargate';
import { createWallet, retrieveAccounts, retrieveSingleAccount } from '../utils/accounts';
import AccountDetails from '../components/AccountDetails';
import styles from '../styles/stylesheet';
import DataBox from '../components/DataBox';
import { getPathKey } from '../utils/misc';
import { useNetworks } from '../context/NetworksContext';
import TxErrorDialog from '../components/TxErrorDialog';
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 = () => {
const [isTxRequested, setIsTxRequested] = useState<boolean>(false);
const [transactionDetails, setTransactionDetails] = useState<TransactionDetails | null>(null);
const [fees, setFees] = useState<string>('');
const [gasLimit, setGasLimit] = useState<string>('');
const [isTxLoading, setIsTxLoading] = useState(false);
const [txError, setTxError] = useState<string | null>(null);
const txEventRef = useRef<MessageEvent | null>(null);
const { networksData } = useNetworks();
const getAccountsData = useCallback(async (chainId: string): Promise<string[]> => {
const targetNetwork = networksData.find(network => network.chainId === chainId);
if (!targetNetwork) {
return [];
}
const accounts = await retrieveAccounts(targetNetwork);
if (!accounts || accounts.length === 0) {
return [];
}
return accounts.map(account => account.address);
}, [networksData]);
const sendMessage = (
source: Window | null,
type: string,
data: any,
origin: string
): void => {
source?.postMessage({ type, data }, origin);
};
const checkSufficientFunds = (amount: string, balance: string) => {
const amountBigNum = BigNumber.from(String(amount));
const balanceBigNum = BigNumber.from(balance);
return balanceBigNum.gt(amountBigNum);
};
useEffect(() => {
const handleGetAccounts = async (event: MessageEvent) => {
if (event.data.type !== 'REQUEST_WALLET_ACCOUNTS') return;
const accountsData = await getAccountsData(event.data.chainId);
if (accountsData.length === 0) {
sendMessage(event.source as Window, 'ERROR', 'Wallet accounts not found', event.origin);
return;
}
sendMessage(event.source as Window, 'WALLET_ACCOUNTS_DATA', accountsData, event.origin);
};
window.addEventListener('message', handleGetAccounts);
return () => {
window.removeEventListener('message', handleGetAccounts);
};
}, [getAccountsData]);
useEffect(() => {
const handleCreateAccounts = async (event: MessageEvent) => {
if (event.data.type !== 'REQUEST_CREATE_OR_GET_ACCOUNTS') return;
let accountsData = await getAccountsData(event.data.chainId);
if (accountsData.length === 0) {
console.log("Accounts not found, creating wallet...");
await createWallet(networksData);
// Re-fetch newly created accounts
accountsData = await getAccountsData(event.data.chainId);
}
sendMessage(event.source as Window, 'WALLET_ACCOUNTS_DATA', accountsData, event.origin);
};
window.addEventListener('message', handleCreateAccounts);
return () => {
window.removeEventListener('message', handleCreateAccounts);
};
}, [networksData, getAccountsData]);
const handleTxRequested = useCallback(
async (event: MessageEvent) => {
try {
if (event.data.type !== 'REQUEST_TX') return;
txEventRef.current = event;
const { chainId, fromAddress, toAddress, amount } = event.data;
const network = networksData.find(net => net.chainId === chainId);
if (!network) {
console.error('Network not found');
throw new Error('Requested network not supported.');
}
const account = await retrieveSingleAccount(network.namespace, network.chainId, fromAddress);
if (!account) {
throw new Error('Account not found for the requested address.');
}
const cosmosPrivKey = (
await getPathKey(`${network.namespace}:${chainId}`, account.index)
).privKey;
const sender = await DirectSecp256k1Wallet.fromKey(
Buffer.from(cosmosPrivKey.split('0x')[1], 'hex'),
network.addressPrefix
);
const client = await SigningStargateClient.connectWithSigner(network.rpcUrl!, sender);
const balance = await client.getBalance(
account.address,
network.nativeDenom!.toLowerCase()
);
const sendMsg = {
typeUrl: '/cosmos.bank.v1beta1.MsgSend',
value: {
fromAddress: fromAddress,
toAddress: toAddress,
amount: [
{
amount: String(amount),
denom: network.nativeDenom!,
},
],
},
};
setTransactionDetails({
chainId,
fromAddress,
toAddress,
amount,
account,
balance: balance.amount,
requestedNetwork: network,
});
if (!checkSufficientFunds(amount, balance.amount)) {
console.log("Insufficient funds detected. Throwing error.");
throw new Error('Insufficient funds');
}
const gasEstimation = await client.simulate(fromAddress, [sendMsg], MEMO);
const gasLimit = String(
Math.round(gasEstimation * Number(process.env.REACT_APP_GAS_ADJUSTMENT))
);
setGasLimit(gasLimit);
const gasPrice = GasPrice.fromString(`${network.gasPrice}${network.nativeDenom}`);
const cosmosFees = calculateFee(Number(gasLimit), gasPrice);
setFees(cosmosFees.amount[0].amount);
setIsTxRequested(true);
} catch (error) {
if (!(error instanceof Error)) {
throw error;
}
setTxError(error.message);
}
}, [networksData]);
useEffect(() => {
window.addEventListener('message', handleTxRequested);
return () => window.removeEventListener('message', handleTxRequested);
}, [handleTxRequested]);
const acceptRequestHandler = async () => {
try {
setIsTxLoading(true);
if (!transactionDetails) {
throw new Error('Tx details not set');
}
const balanceBigNum = BigNumber.from(transactionDetails.balance);
const amountBigNum = BigNumber.from(String(transactionDetails.amount));
if (amountBigNum.gte(balanceBigNum)) {
throw new Error('Insufficient funds');
}
const cosmosPrivKey = (
await getPathKey(`${transactionDetails.requestedNetwork.namespace}:${transactionDetails.chainId}`, transactionDetails.account.index)
).privKey;
const sender = await DirectSecp256k1Wallet.fromKey(
Buffer.from(cosmosPrivKey.split('0x')[1], 'hex'),
transactionDetails.requestedNetwork.addressPrefix
);
const client = await SigningStargateClient.connectWithSigner(
transactionDetails.requestedNetwork.rpcUrl!,
sender
);
const fee = calculateFee(
Number(gasLimit),
GasPrice.fromString(`${transactionDetails.requestedNetwork.gasPrice}${transactionDetails.requestedNetwork.nativeDenom}`)
);
const txResult = await client.sendTokens(
transactionDetails.fromAddress,
transactionDetails.toAddress,
[{ amount: String(transactionDetails.amount), denom: transactionDetails.requestedNetwork.nativeDenom! }],
fee
);
const event = txEventRef.current;
if (event?.source) {
sendMessage(event.source as Window, 'TRANSACTION_RESPONSE', txResult.transactionHash, event.origin);
} else {
console.error('No event source available to send message');
}
} catch (error) {
if (!(error instanceof Error)) {
throw error;
}
setTxError(error.message);
} finally {
setIsTxLoading(false);
}
};
const rejectRequestHandler = () => {
const event = txEventRef.current;
setIsTxRequested(false);
setTransactionDetails(null);
if (event?.source) {
sendMessage(event.source as Window, 'TRANSACTION_RESPONSE', null, event.origin);
} else {
console.error('No event source available to send message');
}
};
return (
<>
{isTxRequested && transactionDetails ? (
<>
<ScrollView contentContainerStyle={styles.appContainer}>
<View style={styles.dataBoxContainer}>
<Text style={styles.dataBoxLabel}>From</Text>
<View style={styles.dataBox}>
<AccountDetails account={transactionDetails.account} />
</View>
</View>
<DataBox
label={`Balance (${transactionDetails.requestedNetwork.nativeDenom})`}
data={
transactionDetails.balance === '' ||
transactionDetails.balance === undefined
? 'Loading balance...'
: `${transactionDetails.balance}`
}
/>
<View style={styles.approveTransfer}>
<DataBox label="To" data={transactionDetails.toAddress} />
<DataBox
label={`Amount (${transactionDetails.requestedNetwork.nativeDenom})`}
data={transactionDetails.amount}
/>
<TextInput
mode="outlined"
label="Fee"
value={fees}
onChangeText={setFees}
style={styles.transactionFeesInput}
/>
<TextInput
mode="outlined"
label="Gas Limit"
value={gasLimit}
onChangeText={value =>
/^\d+$/.test(value) ? setGasLimit(value) : null
}
/>
</View>
</ScrollView>
<View style={styles.buttonContainer}>
<Button
mode="contained"
onPress={acceptRequestHandler}
loading={isTxLoading}
disabled={!transactionDetails.balance || !fees || isTxLoading}
>
{isTxLoading ? 'Processing' : 'Yes'}
</Button>
<Button
mode="contained"
onPress={rejectRequestHandler}
buttonColor="#B82B0D"
disabled={isTxLoading}
>
No
</Button>
</View>
</>
) : (
<View style={styles.spinnerContainer}>
<View style={{marginTop: 50}}></View>
<ActivityIndicator size="large" color="#0000ff" />
</View>
)}
<TxErrorDialog
error={txError!}
visible={!!txError}
hideDialog={() => {
setTxError(null)
if (window.parent) {
sendMessage(window.parent, 'TRANSACTION_RESPONSE', null, '*');
sendMessage(window.parent, 'closeIframe', null, '*');
}
}}
/>
</>
);
};

View File

@ -288,7 +288,7 @@ const styles = StyleSheet.create({
fontSize: 18, fontSize: 18,
fontWeight: "bold", fontWeight: "bold",
marginBottom: 3, marginBottom: 3,
color: "black", color: "white",
}, },
dataBox: { dataBox: {
borderWidth: 1, borderWidth: 1,

View File

@ -36,6 +36,7 @@ export type StackParamsList = {
requestEvent: Web3WalletTypes.SessionRequest; requestEvent: Web3WalletTypes.SessionRequest;
requestSessionData: SessionTypes.Struct; requestSessionData: SessionTypes.Struct;
}; };
WalletEmbed: undefined;
}; };
export type Account = { export type Account = {

View File

@ -3,20 +3,31 @@ import { EIP155_CHAINS } from './wallet-connect/EIP155Data';
export const EIP155 = 'eip155'; export const EIP155 = 'eip155';
export const COSMOS = 'cosmos'; export const COSMOS = 'cosmos';
export const LACONIC = 'laconic';
export const DEFAULT_NETWORKS = [ export const DEFAULT_NETWORKS = [
{ {
chainId: 'laconic_9000-1', chainId: 'laconic-testnet-2',
networkName: 'laconicd', networkName: 'laconicd testnet-2',
namespace: LACONIC, namespace: COSMOS,
rpcUrl: process.env.REACT_APP_LACONICD_RPC_URL!, rpcUrl: process.env.REACT_APP_LACONICD_RPC_URL!,
blockExplorerUrl: '', blockExplorerUrl: '',
nativeDenom: 'alnt', nativeDenom: 'alnt',
addressPrefix: 'laconic', addressPrefix: 'laconic',
coinType: '118', coinType: '118',
gasPrice: '1', gasPrice: '0.001',
isDefault: true, 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,
},
{ {
chainId: '1', chainId: '1',
networkName: EIP155_CHAINS['eip155:1'].name, networkName: EIP155_CHAINS['eip155:1'].name,

View File

@ -8,7 +8,7 @@ import { Account, NetworksDataState } from '../../types';
import { EIP155_SIGNING_METHODS } from './EIP155Data'; import { EIP155_SIGNING_METHODS } from './EIP155Data';
import { mergeWith } from 'lodash'; import { mergeWith } from 'lodash';
import { retrieveAccounts } from '../accounts'; import { retrieveAccounts } from '../accounts';
import { COSMOS, EIP155, LACONIC } from '../constants'; import { COSMOS, EIP155 } from '../constants';
import { NETWORK_METHODS } from './common-data'; import { NETWORK_METHODS } from './common-data';
import { COSMOS_METHODS } from './COSMOSData'; import { COSMOS_METHODS } from './COSMOSData';
@ -131,20 +131,6 @@ export const getNamespaces = async (
], ],
accounts: requiredAddresses.filter(account => account.includes(COSMOS)), accounts: requiredAddresses.filter(account => account.includes(COSMOS)),
}, },
laconic: {
chains: walletConnectChains.filter(chain => chain.includes(LACONIC)),
methods: [
...Object.values(COSMOS_METHODS),
...Object.values(NETWORK_METHODS),
...(optionalNamespaces.laconic?.methods ?? []),
...(requiredNamespaces.laconic?.methods ?? []),
],
events: [
...(optionalNamespaces.laconic?.events ?? []),
...(requiredNamespaces.laconic?.events ?? []),
],
accounts: requiredAddresses.filter(account => account.includes(LACONIC)),
},
}; };
return newNamespaces; return newNamespaces;
@ -175,12 +161,6 @@ export const getNamespaces = async (
events: [], events: [],
accounts: [], accounts: [],
}, },
laconic: {
chains: [],
methods: [],
events: [],
accounts: [],
},
}; };
case COSMOS: case COSMOS:
return { return {
@ -206,43 +186,6 @@ export const getNamespaces = async (
events: [], events: [],
accounts: [], accounts: [],
}, },
laconic: {
chains: [],
methods: [],
events: [],
accounts: [],
},
};
case LACONIC:
return {
laconic: {
chains: [namespaceChainId],
methods: [
...Object.values(COSMOS_METHODS),
...Object.values(NETWORK_METHODS),
...(optionalNamespaces.laconic?.methods ?? []),
...(requiredNamespaces.laconic?.methods ?? []),
],
events: [
...(optionalNamespaces.laconic?.events ?? []),
...(requiredNamespaces.laconic?.events ?? []),
],
accounts: accounts.map(laconicAccount => {
return `${namespaceChainId}:${laconicAccount.address}`;
}),
},
eip155: {
chains: [],
methods: [],
events: [],
accounts: [],
},
cosmos: {
chains: [],
methods: [],
events: [],
accounts: [],
},
}; };
default: default:
break; break;

View File

@ -6,6 +6,7 @@ services:
environment: environment:
CERC_SCRIPT_DEBUG: ${CERC_SCRIPT_DEBUG} CERC_SCRIPT_DEBUG: ${CERC_SCRIPT_DEBUG}
WALLET_CONNECT_ID: ${WALLET_CONNECT_ID} WALLET_CONNECT_ID: ${WALLET_CONNECT_ID}
WALLET_CONNECT_VERIFY_CODE: ${WALLET_CONNECT_VERIFY_CODE}
CERC_DEFAULT_GAS_PRICE: ${CERC_DEFAULT_GAS_PRICE:-0.025} CERC_DEFAULT_GAS_PRICE: ${CERC_DEFAULT_GAS_PRICE:-0.025}
CERC_GAS_ADJUSTMENT: ${CERC_GAS_ADJUSTMENT:-2} CERC_GAS_ADJUSTMENT: ${CERC_GAS_ADJUSTMENT:-2}
CERC_LACONICD_RPC_URL: ${CERC_LACONICD_RPC_URL:-https://laconicd.laconic.com} CERC_LACONICD_RPC_URL: ${CERC_LACONICD_RPC_URL:-https://laconicd.laconic.com}

View File

@ -18,4 +18,13 @@ REACT_APP_GAS_ADJUSTMENT=$CERC_GAS_ADJUSTMENT \
REACT_APP_LACONICD_RPC_URL=$CERC_LACONICD_RPC_URL \ REACT_APP_LACONICD_RPC_URL=$CERC_LACONICD_RPC_URL \
yarn build yarn build
# Define the directory and file path
FILE_PATH="/app/build/.well-known/walletconnect.txt"
# Create the directory if it doesn't exist
mkdir -p "$(dirname "$FILE_PATH")"
# Write verification code to the file
echo "$WALLET_CONNECT_VERIFY_CODE" > "$FILE_PATH"
# Serve build dir
http-server --proxy http://localhost:80? -p 80 /app/build http-server --proxy http://localhost:80? -p 80 /app/build

View File

@ -51,6 +51,9 @@ Instructions for running the `laconic-wallet-web` using [laconic-so](https://git
# Optional # Optional
# WalletConnect code for hostname verification
WALLET_CONNECT_VERIFY_CODE=
# Default gas price for txs (default: 0.025) # Default gas price for txs (default: 0.025)
CERC_DEFAULT_GAS_PRICE= CERC_DEFAULT_GAS_PRICE=