forked from cerc-io/laconic-wallet-web
Setup react native paper (#1)
* Basic setup for react native web * Comment unsupported react-native code * Add interface for keychain methods * Fix storeNetworkData method * Use mui dialog component * Modify key store file name * Fix add network and edit network screens * Fix sign message screen * Use light theme --------- Co-authored-by: Nabarun <nabarun@deepstacksoft.com>
This commit is contained in:
parent
5b99becee0
commit
640155aa4a
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
42
config-overrides.js
Normal file
42
config-overrides.js
Normal file
@ -0,0 +1,42 @@
|
||||
// TODO: Use Typescript
|
||||
|
||||
module.exports = function override(config, env) {
|
||||
config.module.rules.push({
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules[/\\](?!react-native-vector-icons)/,
|
||||
use: {
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
// Disable reading babel configuration
|
||||
babelrc: false,
|
||||
configFile: false,
|
||||
|
||||
// The configuration for compilation
|
||||
presets: [
|
||||
["@babel/preset-env", { useBuiltIns: "usage", "corejs": "3" }],
|
||||
"@babel/preset-react",
|
||||
"@babel/preset-flow",
|
||||
"@babel/preset-typescript",
|
||||
],
|
||||
plugins: [
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-proposal-object-rest-spread"
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
config.module.rules.push({
|
||||
test: /\.(jpg|png|woff|woff2|eot|ttf|svg)$/,
|
||||
type: 'asset/resource'
|
||||
})
|
||||
|
||||
config.resolve.fallback = {
|
||||
crypto: require.resolve("crypto-browserify"),
|
||||
stream: require.resolve("stream-browserify"),
|
||||
}
|
||||
|
||||
config.resolve.alias['react-native$'] = require.resolve('react-native-web');
|
||||
|
||||
return config;
|
||||
};
|
96
package.json
Normal file
96
package.json
Normal file
@ -0,0 +1,96 @@
|
||||
{
|
||||
"name": "web-wallet",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@cerc-io/registry-sdk": "^0.2.2",
|
||||
"@cosmjs/amino": "^0.32.3",
|
||||
"@cosmjs/crypto": "^0.32.3",
|
||||
"@cosmjs/proto-signing": "^0.32.3",
|
||||
"@cosmjs/stargate": "^0.32.3",
|
||||
"@emotion/react": "^11.13.0",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@ethersproject/shims": "^5.7.0",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@json-rpc-tools/utils": "^1.7.6",
|
||||
"@mui/material": "^5.16.4",
|
||||
"@react-native-async-storage/async-storage": "^1.22.3",
|
||||
"@react-native-community/netinfo": "^11.3.1",
|
||||
"@react-navigation/elements": "^1.3.30",
|
||||
"@react-navigation/native": "^6.1.10",
|
||||
"@react-navigation/native-stack": "^6.9.18",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^13.0.0",
|
||||
"@testing-library/user-event": "^13.2.1",
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/node": "^16.7.13",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"assert": "^2.1.0",
|
||||
"chain-registry": "^1.41.2",
|
||||
"cosmjs-types": "^0.9.0",
|
||||
"ethers": "5.7.2",
|
||||
"http-browserify": "^1.7.0",
|
||||
"https-browserify": "^1.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"patch-package": "^8.0.0",
|
||||
"react": "^18.3.1",
|
||||
"react-art": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.51.2",
|
||||
"react-native": "^0.74.3",
|
||||
"react-native-config": "^1.5.1",
|
||||
"react-native-get-random-values": "^1.10.0",
|
||||
"react-native-paper": "^5.12.3",
|
||||
"react-native-quick-base64": "^2.0.8",
|
||||
"react-native-quick-crypto": "^0.6.1",
|
||||
"react-native-safe-area-context": "^4.10.8",
|
||||
"react-native-screens": "^3.29.0",
|
||||
"react-native-svg": "^15.1.0",
|
||||
"react-native-url-polyfill": "^2.0.0",
|
||||
"react-native-vector-icons": "^10.1.0",
|
||||
"react-native-web": "^0.19.12",
|
||||
"react-scripts": "5.0.1",
|
||||
"text-encoding-polyfill": "^0.6.7",
|
||||
"typescript": "^4.4.2",
|
||||
"use-debounce": "^10.0.0",
|
||||
"web-vitals": "^2.1.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-app-rewired start",
|
||||
"build": "react-app-rewired build",
|
||||
"test": "react-app-rewired test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.20.7",
|
||||
"@babel/preset-env": "^7.24.8",
|
||||
"@babel/preset-flow": "^7.24.7",
|
||||
"@babel/preset-react": "^7.24.7",
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"@types/lodash": "^4.17.7",
|
||||
"babel-loader": "^9.1.3",
|
||||
"core-js": "^3.37.1",
|
||||
"react-app-rewired": "^2.2.1"
|
||||
}
|
||||
}
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
43
public/index.html
Normal file
43
public/index.html
Normal file
@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
BIN
public/logo192.png
Normal file
BIN
public/logo192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
BIN
public/logo512.png
Normal file
BIN
public/logo512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.4 KiB |
25
public/manifest.json
Normal file
25
public/manifest.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
3
public/robots.txt
Normal file
3
public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
38
src/App.css
Normal file
38
src/App.css
Normal file
@ -0,0 +1,38 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
9
src/App.test.tsx
Normal file
9
src/App.test.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
328
src/App.tsx
Normal file
328
src/App.tsx
Normal file
@ -0,0 +1,328 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Button, Snackbar, Text } from 'react-native-paper';
|
||||
// import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
// import { TxBody, AuthInfo } from 'cosmjs-types/cosmos/tx/v1beta1/tx';
|
||||
|
||||
// import { SignClientTypes } from '@walletconnect/types';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import {
|
||||
NativeStackNavigationProp,
|
||||
createNativeStackNavigator,
|
||||
} from '@react-navigation/native-stack';
|
||||
// import { getSdkError } from '@walletconnect/utils';
|
||||
// import { Web3WalletTypes } from '@walletconnect/web3wallet';
|
||||
// import { formatJsonRpcResult } from '@json-rpc-tools/utils';
|
||||
|
||||
// import PairingModal from './components/PairingModal';
|
||||
// import { useWalletConnect } from './context/WalletConnectContext';
|
||||
import { useAccounts } from './context/AccountsContext';
|
||||
import InvalidPath from './screens/InvalidPath';
|
||||
import SignMessage from './screens/SignMessage';
|
||||
import HomeScreen from './screens/HomeScreen';
|
||||
import SignRequest from './screens/SignRequest';
|
||||
import AddSession from './screens/AddSession';
|
||||
// import WalletConnect from './screens/WalletConnect';
|
||||
import ApproveTransaction from './screens/ApproveTransaction';
|
||||
import { StackParamsList } from './types';
|
||||
// import { web3wallet } from './utils/wallet-connect/WalletConnectUtils';
|
||||
// import { EIP155_SIGNING_METHODS } from './utils/wallet-connect/EIP155Data';
|
||||
// import { getSignParamsMessage } from './utils/wallet-connect/helpers';
|
||||
import ApproveTransfer from './screens/ApproveTransfer';
|
||||
import AddNetwork from './screens/AddNetwork';
|
||||
import EditNetwork from './screens/EditNetwork';
|
||||
// import { COSMOS, EIP155 } from './utils/constants';
|
||||
import { useNetworks } from './context/NetworksContext';
|
||||
// import { NETWORK_METHODS } from './utils/wallet-connect/common-data';
|
||||
// import { COSMOS_METHODS } from './utils/wallet-connect/COSMOSData';
|
||||
|
||||
const Stack = createNativeStackNavigator<StackParamsList>();
|
||||
|
||||
const App = (): React.JSX.Element => {
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<StackParamsList>>();
|
||||
|
||||
// const { setActiveSessions } = useWalletConnect();
|
||||
const { accounts, setCurrentIndex } = useAccounts();
|
||||
const { networksData, selectedNetwork, setSelectedNetwork } = useNetworks();
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [toastVisible, setToastVisible] = useState(false);
|
||||
// const [currentProposal, setCurrentProposal] = useState<
|
||||
// SignClientTypes.EventArguments['session_proposal'] | undefined
|
||||
// >();
|
||||
|
||||
// const onSessionProposal = useCallback(
|
||||
// async (proposal: SignClientTypes.EventArguments['session_proposal']) => {
|
||||
// if (!accounts.length || !accounts.length) {
|
||||
// const { id } = proposal;
|
||||
// await web3wallet!.rejectSession({
|
||||
// id,
|
||||
// reason: getSdkError('UNSUPPORTED_ACCOUNTS'),
|
||||
// });
|
||||
// return;
|
||||
// }
|
||||
// setModalVisible(true);
|
||||
// setCurrentProposal(proposal);
|
||||
// },
|
||||
// [accounts],
|
||||
// );
|
||||
|
||||
// const onSessionRequest = useCallback(
|
||||
// async (requestEvent: Web3WalletTypes.SessionRequest) => {
|
||||
// const { topic, params, id } = requestEvent;
|
||||
// const { request } = params;
|
||||
|
||||
// const requestSessionData =
|
||||
// web3wallet!.engine.signClient.session.get(topic);
|
||||
// switch (request.method) {
|
||||
// case NETWORK_METHODS.GET_NETWORKS:
|
||||
// const currentNetworkId = networksData.find(
|
||||
// networkData => networkData.networkId === selectedNetwork!.networkId,
|
||||
// )?.networkId;
|
||||
|
||||
// const networkNamesData = networksData.map(networkData => {
|
||||
// return {
|
||||
// id: networkData.networkId,
|
||||
// name: networkData.networkName,
|
||||
// };
|
||||
// });
|
||||
|
||||
// const formattedResponse = formatJsonRpcResult(id, {
|
||||
// currentNetworkId,
|
||||
// networkNamesData,
|
||||
// });
|
||||
|
||||
// await web3wallet!.respondSessionRequest({
|
||||
// topic,
|
||||
// response: formattedResponse,
|
||||
// });
|
||||
// break;
|
||||
|
||||
// case NETWORK_METHODS.CHANGE_NETWORK:
|
||||
// const networkNameData = request.params[0];
|
||||
// const network = networksData.find(
|
||||
// networkData => networkData.networkId === networkNameData.id,
|
||||
// );
|
||||
// setCurrentIndex(0);
|
||||
// setSelectedNetwork(network);
|
||||
|
||||
// const response = formatJsonRpcResult(id, {
|
||||
// response: 'true',
|
||||
// });
|
||||
|
||||
// await web3wallet!.respondSessionRequest({
|
||||
// topic,
|
||||
// response: response,
|
||||
// });
|
||||
// break;
|
||||
|
||||
// case EIP155_SIGNING_METHODS.ETH_SEND_TRANSACTION:
|
||||
// navigation.navigate('ApproveTransfer', {
|
||||
// transaction: request.params[0],
|
||||
// requestEvent,
|
||||
// requestSessionData,
|
||||
// });
|
||||
// break;
|
||||
|
||||
// case EIP155_SIGNING_METHODS.PERSONAL_SIGN:
|
||||
// navigation.navigate('SignRequest', {
|
||||
// namespace: EIP155,
|
||||
// address: request.params[1],
|
||||
// message: getSignParamsMessage(request.params),
|
||||
// requestEvent,
|
||||
// requestSessionData,
|
||||
// });
|
||||
// break;
|
||||
|
||||
// case COSMOS_METHODS.COSMOS_SIGN_DIRECT:
|
||||
// const message = {
|
||||
// txbody: TxBody.toJSON(
|
||||
// TxBody.decode(
|
||||
// Uint8Array.from(
|
||||
// Buffer.from(request.params.signDoc.bodyBytes, 'hex'),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// authInfo: AuthInfo.toJSON(
|
||||
// AuthInfo.decode(
|
||||
// Uint8Array.from(
|
||||
// Buffer.from(request.params.signDoc.authInfoBytes, 'hex'),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// };
|
||||
// navigation.navigate('SignRequest', {
|
||||
// namespace: COSMOS,
|
||||
// address: request.params.signerAddress,
|
||||
// message: JSON.stringify(message, undefined, 2),
|
||||
// requestEvent,
|
||||
// requestSessionData,
|
||||
// });
|
||||
// break;
|
||||
|
||||
// case COSMOS_METHODS.COSMOS_SIGN_AMINO:
|
||||
// navigation.navigate('SignRequest', {
|
||||
// namespace: COSMOS,
|
||||
// address: request.params.signerAddress,
|
||||
// message: request.params.signDoc.memo,
|
||||
// requestEvent,
|
||||
// requestSessionData,
|
||||
// });
|
||||
// break;
|
||||
|
||||
// case COSMOS_METHODS.COSMOS_SEND_TOKENS:
|
||||
// navigation.navigate('ApproveTransfer', {
|
||||
// transaction: request.params[0],
|
||||
// requestEvent,
|
||||
// requestSessionData,
|
||||
// });
|
||||
// break;
|
||||
|
||||
// case COSMOS_METHODS.COSMOS_SEND_TRANSACTION:
|
||||
// const { transactionMessage, signer } = request.params;
|
||||
// navigation.navigate('ApproveTransaction', {
|
||||
// transactionMessage,
|
||||
// signer,
|
||||
// requestEvent,
|
||||
// requestSessionData,
|
||||
// });
|
||||
// break;
|
||||
|
||||
// default:
|
||||
// throw new Error('Invalid method');
|
||||
// }
|
||||
// },
|
||||
// [
|
||||
// navigation,
|
||||
// networksData,
|
||||
// setSelectedNetwork,
|
||||
// setCurrentIndex,
|
||||
// selectedNetwork,
|
||||
// ],
|
||||
// );
|
||||
|
||||
// const onSessionDelete = useCallback(() => {
|
||||
// const sessions = web3wallet!.getActiveSessions();
|
||||
// setActiveSessions(sessions);
|
||||
// }, [setActiveSessions]);
|
||||
|
||||
// useEffect(() => {
|
||||
// web3wallet?.on('session_proposal', onSessionProposal);
|
||||
// web3wallet?.on('session_request', onSessionRequest);
|
||||
// web3wallet?.on('session_delete', onSessionDelete);
|
||||
// return () => {
|
||||
// web3wallet?.off('session_proposal', onSessionProposal);
|
||||
// web3wallet?.off('session_request', onSessionRequest);
|
||||
// web3wallet?.off('session_delete', onSessionDelete);
|
||||
// };
|
||||
// });
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Navigator>
|
||||
<Stack.Screen
|
||||
name="Laconic"
|
||||
component={HomeScreen}
|
||||
options={{
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
headerTitle: () => <Text variant="titleLarge">Laconic Wallet</Text>,
|
||||
headerBackVisible: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="SignMessage"
|
||||
component={SignMessage}
|
||||
options={{
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
headerTitle: () => <Text variant="titleLarge">Sign Message</Text>,
|
||||
}}
|
||||
/>
|
||||
{/* <Stack.Screen
|
||||
name="SignRequest"
|
||||
component={SignRequest}
|
||||
options={{
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
headerTitle: () => <Text variant="titleLarge">Sign Request</Text>,
|
||||
}}
|
||||
/> */}
|
||||
<Stack.Screen
|
||||
name="InvalidPath"
|
||||
component={InvalidPath}
|
||||
options={{
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
headerTitle: () => <Text variant="titleLarge">Bad Request</Text>,
|
||||
headerBackVisible: false,
|
||||
}}
|
||||
/>
|
||||
{/* <Stack.Screen
|
||||
name="WalletConnect"
|
||||
component={WalletConnect}
|
||||
options={{
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
headerTitle: () => <Text variant="titleLarge">WalletConnect</Text>,
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
headerRight: () => (
|
||||
<Button
|
||||
onPress={() => {
|
||||
navigation.navigate('AddSession');
|
||||
}}>
|
||||
{<Icon name={'qrcode-scan'} size={20} />}
|
||||
</Button>
|
||||
),
|
||||
}}
|
||||
/> */}
|
||||
|
||||
<Stack.Screen
|
||||
name="AddSession"
|
||||
component={AddSession}
|
||||
options={{
|
||||
title: 'New session',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* <Stack.Screen
|
||||
name="ApproveTransfer"
|
||||
component={ApproveTransfer}
|
||||
options={{
|
||||
title: 'Approve transfer',
|
||||
}}
|
||||
/> */}
|
||||
<Stack.Screen
|
||||
name="AddNetwork"
|
||||
component={AddNetwork}
|
||||
options={{
|
||||
title: 'Add Network',
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="EditNetwork"
|
||||
component={EditNetwork}
|
||||
options={{
|
||||
title: 'Edit Network',
|
||||
}}
|
||||
/>
|
||||
{/* <Stack.Screen
|
||||
name="ApproveTransaction"
|
||||
component={ApproveTransaction}
|
||||
options={{
|
||||
title: 'Approve Transaction',
|
||||
}}
|
||||
/> */}
|
||||
</Stack.Navigator>
|
||||
{/* <PairingModal
|
||||
visible={modalVisible}
|
||||
setModalVisible={setModalVisible}
|
||||
currentProposal={currentProposal}
|
||||
setCurrentProposal={setCurrentProposal}
|
||||
setToastVisible={setToastVisible}
|
||||
/> */}
|
||||
<Snackbar
|
||||
visible={toastVisible}
|
||||
onDismiss={() => setToastVisible(false)}
|
||||
duration={3000}>
|
||||
Session approved
|
||||
</Snackbar>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
BIN
src/assets/WalletConnect-Icon-Blueberry.png
Normal file
BIN
src/assets/WalletConnect-Icon-Blueberry.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
1
src/assets/ethereum-chains.json
Normal file
1
src/assets/ethereum-chains.json
Normal file
File diff suppressed because one or more lines are too long
31
src/components/AccountDetails.tsx
Normal file
31
src/components/AccountDetails.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { Text } from 'react-native-paper';
|
||||
|
||||
import { Account } from '../types';
|
||||
import styles from '../styles/stylesheet';
|
||||
|
||||
interface AccountDetailsProps {
|
||||
account: Account | undefined;
|
||||
}
|
||||
|
||||
const AccountDetails: React.FC<AccountDetailsProps> = ({ account }) => {
|
||||
return (
|
||||
<View style={styles.accountContainer}>
|
||||
<Text variant="bodyLarge" selectable={true}>
|
||||
<Text style={styles.highlight}>Address: </Text>
|
||||
{account?.address}
|
||||
</Text>
|
||||
<Text variant="bodyLarge" selectable={true}>
|
||||
<Text style={styles.highlight}>Public Key: </Text>
|
||||
{account?.pubKey}
|
||||
</Text>
|
||||
<Text variant="bodyLarge">
|
||||
<Text style={styles.highlight}>HD Path: </Text>
|
||||
{account?.hdPath}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountDetails;
|
229
src/components/Accounts.tsx
Normal file
229
src/components/Accounts.tsx
Normal file
@ -0,0 +1,229 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ScrollView, TouchableOpacity, View } from 'react-native';
|
||||
import { Button, List, Text, useTheme } from 'react-native-paper';
|
||||
// import { setInternetCredentials } from 'react-native-keychain';
|
||||
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
|
||||
import { StackParamsList, Account } from '../types';
|
||||
import { addAccount } from '../utils/accounts';
|
||||
import styles from '../styles/stylesheet';
|
||||
import HDPathDialog from './HDPathDialog';
|
||||
import AccountDetails from './AccountDetails';
|
||||
import { useAccounts } from '../context/AccountsContext';
|
||||
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
|
||||
import { useNetworks } from '../context/NetworksContext';
|
||||
import ConfirmDialog from './ConfirmDialog';
|
||||
import { getNamespaces } from '../utils/wallet-connect/helpers';
|
||||
import ShowPKDialog from './ShowPKDialog';
|
||||
|
||||
const Accounts = () => {
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<StackParamsList>>();
|
||||
|
||||
const { accounts, setAccounts, setCurrentIndex, currentIndex } =
|
||||
useAccounts();
|
||||
const { networksData, selectedNetwork, setNetworksData, setSelectedNetwork } =
|
||||
useNetworks();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [isAccountCreating, setIsAccountCreating] = useState(false);
|
||||
const [hdDialog, setHdDialog] = useState(false);
|
||||
const [pathCode, setPathCode] = useState('');
|
||||
const [deleteNetworkDialog, setDeleteNetworkDialog] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const handlePress = () => setExpanded(!expanded);
|
||||
|
||||
const hideDeleteNetworkDialog = () => setDeleteNetworkDialog(false);
|
||||
|
||||
const updateAccounts = (account: Account) => {
|
||||
setAccounts([...accounts, account]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const updateSessions = async () => {
|
||||
const sessions = (web3wallet && web3wallet.getActiveSessions()) || {};
|
||||
// Iterate through each session
|
||||
|
||||
for (const topic in sessions) {
|
||||
const session = sessions[topic];
|
||||
const { optionalNamespaces, requiredNamespaces } = session;
|
||||
|
||||
const updatedNamespaces = await getNamespaces(
|
||||
optionalNamespaces,
|
||||
requiredNamespaces,
|
||||
networksData,
|
||||
selectedNetwork!,
|
||||
accounts,
|
||||
currentIndex,
|
||||
);
|
||||
|
||||
if (!updatedNamespaces) {
|
||||
return;
|
||||
}
|
||||
|
||||
await web3wallet!.updateSession({
|
||||
topic,
|
||||
namespaces: updatedNamespaces,
|
||||
});
|
||||
}
|
||||
};
|
||||
// Call the updateSessions function when the 'accounts' dependency changes
|
||||
updateSessions();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [accounts]);
|
||||
|
||||
const addAccountHandler = async () => {
|
||||
setIsAccountCreating(true);
|
||||
const newAccount = await addAccount(selectedNetwork!);
|
||||
setIsAccountCreating(false);
|
||||
if (newAccount) {
|
||||
updateAccounts(newAccount);
|
||||
setCurrentIndex(newAccount.index);
|
||||
}
|
||||
};
|
||||
|
||||
const renderAccountItems = () =>
|
||||
accounts.map(account => (
|
||||
<List.Item
|
||||
key={account.index}
|
||||
title={`Account ${account.index + 1}`}
|
||||
onPress={() => {
|
||||
setCurrentIndex(account.index);
|
||||
setExpanded(false);
|
||||
}}
|
||||
/>
|
||||
));
|
||||
|
||||
const handleRemove = async () => {
|
||||
const updatedNetworks = networksData.filter(
|
||||
networkData => selectedNetwork!.networkId !== networkData.networkId,
|
||||
);
|
||||
|
||||
// await setInternetCredentials(
|
||||
// 'networks',
|
||||
// '_',
|
||||
// JSON.stringify(updatedNetworks),
|
||||
// );
|
||||
|
||||
setSelectedNetwork(updatedNetworks[0]);
|
||||
setCurrentIndex(0);
|
||||
setDeleteNetworkDialog(false);
|
||||
setNetworksData(updatedNetworks);
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
<View>
|
||||
<HDPathDialog
|
||||
visible={hdDialog}
|
||||
hideDialog={() => setHdDialog(false)}
|
||||
updateAccounts={updateAccounts}
|
||||
pathCode={pathCode}
|
||||
/>
|
||||
<List.Accordion
|
||||
title={`Account ${currentIndex + 1}`}
|
||||
expanded={expanded}
|
||||
onPress={handlePress}>
|
||||
{renderAccountItems()}
|
||||
</List.Accordion>
|
||||
|
||||
<View style={styles.addAccountButton}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={addAccountHandler}
|
||||
loading={isAccountCreating}
|
||||
disabled={isAccountCreating}>
|
||||
{isAccountCreating ? 'Adding' : 'Add Account'}
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<View style={styles.addAccountButton}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => {
|
||||
setHdDialog(true);
|
||||
setPathCode(`m/44'/${selectedNetwork!.coinType}'/`);
|
||||
}}>
|
||||
Add Account from HD path
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<AccountDetails account={accounts[currentIndex]} />
|
||||
|
||||
<View style={styles.signLink}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
navigation.navigate('SignMessage', {
|
||||
selectedNamespace: selectedNetwork!.namespace,
|
||||
selectedChainId: selectedNetwork!.chainId,
|
||||
accountInfo: accounts[currentIndex],
|
||||
});
|
||||
}}>
|
||||
<Text
|
||||
variant="titleSmall"
|
||||
style={[styles.hyperlink, { color: theme.colors.primary }]}>
|
||||
Sign Message
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.signLink}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
navigation.navigate('AddNetwork');
|
||||
}}>
|
||||
<Text
|
||||
variant="titleSmall"
|
||||
style={[styles.hyperlink, { color: theme.colors.primary }]}>
|
||||
Add Network
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.signLink}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
navigation.navigate('EditNetwork', {
|
||||
selectedNetwork: selectedNetwork!,
|
||||
});
|
||||
}}>
|
||||
<Text
|
||||
variant="titleSmall"
|
||||
style={[styles.hyperlink, { color: theme.colors.primary }]}>
|
||||
Edit Network
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{!selectedNetwork!.isDefault && (
|
||||
<View style={styles.signLink}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
setDeleteNetworkDialog(true);
|
||||
}}>
|
||||
<Text
|
||||
variant="titleSmall"
|
||||
style={[styles.hyperlink, { color: theme.colors.primary }]}>
|
||||
Delete Network
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
<ConfirmDialog
|
||||
title="Delete Network"
|
||||
visible={deleteNetworkDialog}
|
||||
hideDialog={hideDeleteNetworkDialog}
|
||||
onConfirm={handleRemove}
|
||||
/>
|
||||
|
||||
<ShowPKDialog />
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
export default Accounts;
|
27
src/components/ConfirmDialog.tsx
Normal file
27
src/components/ConfirmDialog.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography } from '@mui/material';
|
||||
import { ResetDialogProps } from '../types';
|
||||
|
||||
const ConfirmDialog = ({
|
||||
title,
|
||||
visible,
|
||||
hideDialog,
|
||||
onConfirm,
|
||||
}: ResetDialogProps) => {
|
||||
return (
|
||||
<Dialog open={visible} onClose={hideDialog}>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="body1">Are you sure?</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button color="error" onClick={onConfirm}>
|
||||
Yes
|
||||
</Button>
|
||||
<Button onClick={hideDialog}>No</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmDialog;
|
27
src/components/CreateWallet.tsx
Normal file
27
src/components/CreateWallet.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { View } from 'react-native';
|
||||
import React from 'react';
|
||||
import { Button } from 'react-native-paper';
|
||||
|
||||
import { CreateWalletProps } from '../types';
|
||||
import styles from '../styles/stylesheet';
|
||||
|
||||
const CreateWallet = ({
|
||||
isWalletCreating,
|
||||
createWalletHandler,
|
||||
}: CreateWalletProps) => {
|
||||
return (
|
||||
<View>
|
||||
<View style={styles.createWalletContainer}>
|
||||
<Button
|
||||
mode="contained"
|
||||
loading={isWalletCreating}
|
||||
disabled={isWalletCreating}
|
||||
onPress={createWalletHandler}>
|
||||
{isWalletCreating ? 'Creating' : 'Create Wallet'}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateWallet;
|
17
src/components/DataBox.tsx
Normal file
17
src/components/DataBox.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
|
||||
import styles from '../styles/stylesheet';
|
||||
|
||||
const DataBox = ({ label, data }: { label: string; data: string }) => {
|
||||
return (
|
||||
<View style={styles.dataBoxContainer}>
|
||||
<Text style={styles.dataBoxLabel}>{label}</Text>
|
||||
<View style={styles.dataBox}>
|
||||
<Text style={styles.dataBoxData}>{data}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataBox;
|
31
src/components/Dialog.tsx
Normal file
31
src/components/Dialog.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { Dialog, DialogActions, DialogContent, DialogTitle, Button, Typography } from '@mui/material';
|
||||
|
||||
import styles from '../styles/stylesheet';
|
||||
import GridView from './Grid';
|
||||
import { CustomDialogProps } from '../types';
|
||||
|
||||
|
||||
const DialogComponent = ({ visible, hideDialog, contentText }: CustomDialogProps) => {
|
||||
const words = contentText.split(' ');
|
||||
|
||||
return (
|
||||
<Dialog open={visible} onClose={hideDialog}>
|
||||
<DialogTitle>Mnemonic</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="h6" component="div" style={{ color: 'rgba(0, 0, 0, 0.87)' }}>
|
||||
Your mnemonic provides full access to your wallet and funds. Make sure to note it down.
|
||||
</Typography>
|
||||
<Typography variant="h6" component="div" style={{ ...styles.dialogWarning }}>
|
||||
Do not share your mnemonic with anyone
|
||||
</Typography>
|
||||
<GridView words={words} />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={hideDialog}>Done</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export { DialogComponent };
|
21
src/components/Grid.tsx
Normal file
21
src/components/Grid.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { Text } from 'react-native-paper';
|
||||
|
||||
import styles from '../styles/stylesheet';
|
||||
import { GridViewProps } from '../types';
|
||||
|
||||
const GridView = ({ words }: GridViewProps) => {
|
||||
return (
|
||||
<View style={styles.gridContainer}>
|
||||
{words.map((word, index) => (
|
||||
<View key={index} style={styles.gridItem}>
|
||||
<Text>{index + 1}. </Text>
|
||||
<Text variant="bodySmall">{word}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default GridView;
|
100
src/components/HDPath.tsx
Normal file
100
src/components/HDPath.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ScrollView, View, Text } from 'react-native';
|
||||
import { Button, TextInput } from 'react-native-paper';
|
||||
|
||||
import { addAccountFromHDPath } from '../utils/accounts';
|
||||
import { Account, NetworksDataState, PathState } from '../types';
|
||||
import styles from '../styles/stylesheet';
|
||||
import { useAccounts } from '../context/AccountsContext';
|
||||
|
||||
const HDPath = ({
|
||||
pathCode,
|
||||
updateAccounts,
|
||||
hideDialog,
|
||||
selectedNetwork,
|
||||
}: {
|
||||
pathCode: string;
|
||||
updateAccounts: (account: Account) => void;
|
||||
hideDialog: () => void;
|
||||
selectedNetwork: NetworksDataState;
|
||||
}) => {
|
||||
const { setCurrentIndex } = useAccounts();
|
||||
const [isAccountCreating, setIsAccountCreating] = useState(false);
|
||||
const [path, setPath] = useState<PathState>({
|
||||
firstNumber: '',
|
||||
secondNumber: '',
|
||||
thirdNumber: '',
|
||||
});
|
||||
|
||||
const handleChange = (key: keyof PathState, value: string) => {
|
||||
if (key === 'secondNumber' && value !== '' && !['0', '1'].includes(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPath({
|
||||
...path,
|
||||
[key]: value.replace(/[^0-9]/g, ''),
|
||||
});
|
||||
};
|
||||
|
||||
const createFromHDPathHandler = async () => {
|
||||
setIsAccountCreating(true);
|
||||
const hdPath =
|
||||
pathCode +
|
||||
`${path.firstNumber}'/${path.secondNumber}/${path.thirdNumber}`;
|
||||
try {
|
||||
const newAccount = await addAccountFromHDPath(hdPath, selectedNetwork);
|
||||
if (newAccount) {
|
||||
updateAccounts(newAccount);
|
||||
setCurrentIndex(newAccount.index);
|
||||
hideDialog();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating account:', error);
|
||||
} finally {
|
||||
setIsAccountCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.HDcontainer}>
|
||||
<View style={styles.HDrowContainer}>
|
||||
<Text style={styles.HDtext}>{pathCode}</Text>
|
||||
<TextInput
|
||||
keyboardType="numeric"
|
||||
mode="outlined"
|
||||
onChangeText={text => handleChange('firstNumber', text)}
|
||||
value={path.firstNumber}
|
||||
style={styles.HDtextInput}
|
||||
/>
|
||||
<Text style={styles.HDtext}>'/</Text>
|
||||
<TextInput
|
||||
keyboardType="numeric"
|
||||
mode="outlined"
|
||||
onChangeText={text => handleChange('secondNumber', text)}
|
||||
value={path.secondNumber}
|
||||
style={styles.HDtextInput}
|
||||
/>
|
||||
<Text style={styles.HDtext}>/</Text>
|
||||
<TextInput
|
||||
keyboardType="numeric"
|
||||
mode="outlined"
|
||||
onChangeText={text => handleChange('thirdNumber', text)}
|
||||
value={path.thirdNumber}
|
||||
style={styles.HDtextInput}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.HDbuttonContainer}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={createFromHDPathHandler}
|
||||
loading={isAccountCreating}
|
||||
disabled={isAccountCreating}>
|
||||
{isAccountCreating ? 'Adding' : 'Add Account'}
|
||||
</Button>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
export default HDPath;
|
31
src/components/HDPathDialog.tsx
Normal file
31
src/components/HDPathDialog.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { Dialog, DialogTitle, DialogContent } from '@mui/material';
|
||||
|
||||
import { HDPathDialogProps } from '../types';
|
||||
import HDPath from './HDPath';
|
||||
import { useNetworks } from '../context/NetworksContext';
|
||||
|
||||
const HDPathDialog = ({
|
||||
visible,
|
||||
hideDialog,
|
||||
updateAccounts,
|
||||
pathCode,
|
||||
}: HDPathDialogProps) => {
|
||||
const { selectedNetwork } = useNetworks();
|
||||
|
||||
return (
|
||||
<Dialog open={visible} onClose={hideDialog}>
|
||||
<DialogTitle>Add account from HD path</DialogTitle>
|
||||
<DialogContent>
|
||||
<HDPath
|
||||
selectedNetwork={selectedNetwork!}
|
||||
pathCode={pathCode}
|
||||
updateAccounts={updateAccounts}
|
||||
hideDialog={hideDialog}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default HDPathDialog;
|
38
src/components/NetworkDropdown.tsx
Normal file
38
src/components/NetworkDropdown.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { List } from 'react-native-paper';
|
||||
|
||||
import { NetworkDropdownProps, NetworksDataState } from '../types';
|
||||
import styles from '../styles/stylesheet';
|
||||
import { useNetworks } from '../context/NetworksContext';
|
||||
|
||||
const NetworkDropdown = ({ updateNetwork }: NetworkDropdownProps) => {
|
||||
const { networksData, selectedNetwork, setSelectedNetwork } = useNetworks();
|
||||
|
||||
const [expanded, setExpanded] = useState<boolean>(false);
|
||||
|
||||
const handleNetworkPress = (networkData: NetworksDataState) => {
|
||||
updateNetwork(networkData);
|
||||
setSelectedNetwork(networkData);
|
||||
setExpanded(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.networkDropdown}>
|
||||
<List.Accordion
|
||||
title={selectedNetwork!.networkName}
|
||||
expanded={expanded}
|
||||
onPress={() => setExpanded(!expanded)}>
|
||||
{networksData.map(networkData => (
|
||||
<List.Item
|
||||
key={networkData.networkId}
|
||||
title={networkData.networkName}
|
||||
onPress={() => handleNetworkPress(networkData)}
|
||||
/>
|
||||
))}
|
||||
</List.Accordion>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export { NetworkDropdown };
|
315
src/components/PairingModal.tsx
Normal file
315
src/components/PairingModal.tsx
Normal file
@ -0,0 +1,315 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Image, View, Modal, ScrollView } from 'react-native';
|
||||
import { Button, Text } from 'react-native-paper';
|
||||
import { SvgUri } from 'react-native-svg';
|
||||
import mergeWith from 'lodash/mergeWith';
|
||||
|
||||
// import { buildApprovedNamespaces, getSdkError } from '@walletconnect/utils';
|
||||
|
||||
import { PairingModalProps } from '../types';
|
||||
import styles from '../styles/stylesheet';
|
||||
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
|
||||
import { useAccounts } from '../context/AccountsContext';
|
||||
import { useWalletConnect } from '../context/WalletConnectContext';
|
||||
import { useNetworks } from '../context/NetworksContext';
|
||||
import { getNamespaces } from '../utils/wallet-connect/helpers';
|
||||
|
||||
const PairingModal = ({
|
||||
visible,
|
||||
currentProposal,
|
||||
setCurrentProposal,
|
||||
setModalVisible,
|
||||
setToastVisible,
|
||||
}: PairingModalProps) => {
|
||||
const { accounts, currentIndex } = useAccounts();
|
||||
const { selectedNetwork, networksData } = useNetworks();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [chainError, setChainError] = useState('');
|
||||
|
||||
// const dappName = currentProposal?.params?.proposer?.metadata.name;
|
||||
// const url = currentProposal?.params?.proposer?.metadata.url;
|
||||
// const icon = currentProposal?.params.proposer?.metadata.icons[0];
|
||||
const dappName = undefined;
|
||||
const url = undefined;
|
||||
const icon = '.svg';
|
||||
|
||||
const [walletConnectData, setWalletConnectData] = useState<{
|
||||
walletConnectMethods: string[];
|
||||
walletConnectEvents: string[];
|
||||
walletConnectChains: string[];
|
||||
}>({
|
||||
walletConnectMethods: [],
|
||||
walletConnectEvents: [],
|
||||
walletConnectChains: [],
|
||||
});
|
||||
|
||||
const [supportedNamespaces, setSupportedNamespaces] = useState<
|
||||
Record<
|
||||
string,
|
||||
{
|
||||
chains: string[];
|
||||
methods: string[];
|
||||
events: string[];
|
||||
accounts: string[];
|
||||
}
|
||||
>
|
||||
>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentProposal) {
|
||||
return;
|
||||
}
|
||||
const { params } = currentProposal;
|
||||
const { requiredNamespaces, optionalNamespaces } = params;
|
||||
|
||||
setWalletConnectData({
|
||||
walletConnectMethods: [],
|
||||
walletConnectEvents: [],
|
||||
walletConnectChains: [],
|
||||
});
|
||||
|
||||
const combinedNamespaces = mergeWith(
|
||||
requiredNamespaces,
|
||||
optionalNamespaces,
|
||||
(obj, src) =>
|
||||
Array.isArray(obj) && Array.isArray(src) ? [...src, ...obj] : undefined,
|
||||
);
|
||||
|
||||
Object.keys(combinedNamespaces).forEach(key => {
|
||||
const { methods, events, chains } = combinedNamespaces[key];
|
||||
|
||||
setWalletConnectData(prevData => {
|
||||
return {
|
||||
walletConnectMethods: [...prevData.walletConnectMethods, ...methods],
|
||||
walletConnectEvents: [...prevData.walletConnectEvents, ...events],
|
||||
walletConnectChains: chains
|
||||
? [...prevData.walletConnectChains, ...chains]
|
||||
: [...prevData.walletConnectChains],
|
||||
};
|
||||
});
|
||||
});
|
||||
}, [currentProposal]);
|
||||
|
||||
const { setActiveSessions } = useWalletConnect();
|
||||
|
||||
useEffect(() => {
|
||||
const getSupportedNamespaces = async () => {
|
||||
if (!currentProposal) {
|
||||
return;
|
||||
}
|
||||
|
||||
// const { optionalNamespaces, requiredNamespaces } = currentProposal.params;
|
||||
const { optionalNamespaces, requiredNamespaces } = currentProposal;
|
||||
|
||||
try {
|
||||
const nameSpaces = await getNamespaces(
|
||||
optionalNamespaces,
|
||||
requiredNamespaces,
|
||||
networksData,
|
||||
selectedNetwork!,
|
||||
accounts,
|
||||
currentIndex,
|
||||
);
|
||||
setSupportedNamespaces(nameSpaces);
|
||||
} catch (err) {
|
||||
setChainError((err as Error).message);
|
||||
|
||||
// const { id } = currentProposal;
|
||||
// await web3wallet!.rejectSession({
|
||||
// id,
|
||||
// reason: getSdkError('UNSUPPORTED_CHAINS'),
|
||||
// });
|
||||
setCurrentProposal(undefined);
|
||||
setWalletConnectData({
|
||||
walletConnectMethods: [],
|
||||
walletConnectEvents: [],
|
||||
walletConnectChains: [],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
getSupportedNamespaces();
|
||||
}, [
|
||||
currentProposal,
|
||||
networksData,
|
||||
selectedNetwork,
|
||||
accounts,
|
||||
currentIndex,
|
||||
setCurrentProposal,
|
||||
setModalVisible,
|
||||
]);
|
||||
|
||||
const namespaces = useMemo(() => {
|
||||
return (
|
||||
currentProposal &&
|
||||
supportedNamespaces
|
||||
// &&
|
||||
// buildApprovedNamespaces({
|
||||
// proposal: currentProposal.params,
|
||||
// supportedNamespaces,
|
||||
// })
|
||||
);
|
||||
}, [currentProposal, supportedNamespaces]);
|
||||
|
||||
const handleAccept = async () => {
|
||||
try {
|
||||
if (currentProposal && namespaces) {
|
||||
setIsLoading(true);
|
||||
const { id } = currentProposal;
|
||||
|
||||
await web3wallet!.approveSession({
|
||||
id,
|
||||
namespaces,
|
||||
});
|
||||
|
||||
const sessions = web3wallet!.getActiveSessions();
|
||||
setIsLoading(false);
|
||||
setActiveSessions(sessions);
|
||||
setModalVisible(false);
|
||||
setToastVisible(true);
|
||||
setCurrentProposal(undefined);
|
||||
setSupportedNamespaces(undefined);
|
||||
setWalletConnectData({
|
||||
walletConnectMethods: [],
|
||||
walletConnectEvents: [],
|
||||
walletConnectChains: [],
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in approve session:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setChainError('');
|
||||
setModalVisible(false);
|
||||
};
|
||||
|
||||
const handleReject = async () => {
|
||||
if (currentProposal) {
|
||||
// const { id } = currentProposal;
|
||||
// await web3wallet!.rejectSession({
|
||||
// id,
|
||||
// reason: getSdkError('USER_REJECTED_METHODS'),
|
||||
// });
|
||||
|
||||
setModalVisible(false);
|
||||
setCurrentProposal(undefined);
|
||||
setWalletConnectData({
|
||||
walletConnectMethods: [],
|
||||
walletConnectEvents: [],
|
||||
walletConnectChains: [],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View>
|
||||
{chainError !== '' ? (
|
||||
<Modal visible={visible} animationType="slide" transparent>
|
||||
<View style={styles.modalContentContainer}>
|
||||
<View style={styles.container}>
|
||||
{icon && (
|
||||
<>
|
||||
{icon.endsWith('.svg') ? (
|
||||
<View style={styles.dappLogo}>
|
||||
<SvgUri height="50" width="50" uri={icon} />
|
||||
</View>
|
||||
) : (
|
||||
<Image style={styles.dappLogo} source={{ uri: icon }} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Text variant="titleMedium">{dappName}</Text>
|
||||
<Text variant="bodyMedium">{url}</Text>
|
||||
<Text variant="titleMedium">{chainError}</Text>
|
||||
</View>
|
||||
<View style={styles.flexRow}>
|
||||
<Button mode="outlined" onPress={handleClose}>
|
||||
Close
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
) : (
|
||||
<Modal visible={visible} animationType="slide" transparent>
|
||||
<View style={styles.modalOuterContainer}>
|
||||
<View style={styles.modalContentContainer}>
|
||||
<ScrollView showsVerticalScrollIndicator={true}>
|
||||
<View style={styles.container}>
|
||||
{icon && (
|
||||
<>
|
||||
{icon.endsWith('.svg') ? (
|
||||
<View style={styles.dappLogo}>
|
||||
<SvgUri height="50" width="50" uri={icon} />
|
||||
</View>
|
||||
) : (
|
||||
<Image style={styles.dappLogo} source={{ uri: icon }} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Text variant="titleMedium">{dappName}</Text>
|
||||
<Text variant="bodyMedium">{url}</Text>
|
||||
<View style={styles.marginVertical8} />
|
||||
<Text variant="titleMedium">Connect to this site?</Text>
|
||||
|
||||
{walletConnectData.walletConnectMethods.length > 0 && (
|
||||
<View>
|
||||
<Text variant="titleMedium">Chains:</Text>
|
||||
{walletConnectData.walletConnectChains.map(chain => (
|
||||
<Text style={styles.centerText} key={chain}>
|
||||
{chain}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{walletConnectData.walletConnectMethods.length > 0 && (
|
||||
<View style={styles.marginVertical8}>
|
||||
<Text variant="titleMedium">Methods Requested:</Text>
|
||||
{walletConnectData.walletConnectMethods.map(method => (
|
||||
<Text style={styles.centerText} key={method}>
|
||||
{method}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{walletConnectData.walletConnectEvents.length > 0 && (
|
||||
<View style={styles.marginVertical8}>
|
||||
<Text variant="titleMedium">Events Requested:</Text>
|
||||
{walletConnectData.walletConnectEvents.map(event => (
|
||||
<Text style={styles.centerText} key={event}>
|
||||
{event}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</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>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default PairingModal;
|
43
src/components/SelectNetworkType.tsx
Normal file
43
src/components/SelectNetworkType.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { Text, List } from 'react-native-paper';
|
||||
|
||||
import styles from '../styles/stylesheet';
|
||||
import { COSMOS, EIP155 } from '../utils/constants';
|
||||
|
||||
const SelectNetworkType = ({
|
||||
updateNetworkType,
|
||||
}: {
|
||||
updateNetworkType: (networkType: string) => void;
|
||||
}) => {
|
||||
const [expanded, setExpanded] = useState<boolean>(false);
|
||||
const [selectedNetwork, setSelectedNetwork] = useState<string>('ETH');
|
||||
|
||||
const networks = ['ETH', 'COSMOS'];
|
||||
|
||||
const handleNetworkPress = (network: string) => {
|
||||
setSelectedNetwork(network);
|
||||
updateNetworkType(network === 'ETH' ? EIP155 : COSMOS);
|
||||
setExpanded(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.networkDropdown}>
|
||||
<Text style={styles.selectNetworkText}>Select Network Type</Text>
|
||||
<List.Accordion
|
||||
title={selectedNetwork}
|
||||
expanded={expanded}
|
||||
onPress={() => setExpanded(!expanded)}>
|
||||
{networks.map(network => (
|
||||
<List.Item
|
||||
key={network}
|
||||
title={network}
|
||||
onPress={() => handleNetworkPress(network)}
|
||||
/>
|
||||
))}
|
||||
</List.Accordion>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export { SelectNetworkType };
|
101
src/components/ShowPKDialog.tsx
Normal file
101
src/components/ShowPKDialog.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
import React, { useState } from 'react';
|
||||
import { TouchableOpacity, View } from 'react-native';
|
||||
import { Button, Typography } from '@mui/material';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
|
||||
import styles from '../styles/stylesheet';
|
||||
import { getPathKey } from '../utils/misc';
|
||||
import { useNetworks } from '../context/NetworksContext';
|
||||
import { useAccounts } from '../context/AccountsContext';
|
||||
import { Text, useTheme } from 'react-native-paper';
|
||||
|
||||
const ShowPKDialog = () => {
|
||||
const { currentIndex } = useAccounts();
|
||||
const { selectedNetwork } = useNetworks();
|
||||
|
||||
const [privateKey, setPrivateKey] = useState<string>();
|
||||
const [showPKDialog, setShowPKDialog] = useState<boolean>(false);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const handleShowPrivateKey = async () => {
|
||||
const pathKey = await getPathKey(
|
||||
`${selectedNetwork!.namespace}:${selectedNetwork!.chainId}`,
|
||||
currentIndex,
|
||||
);
|
||||
|
||||
setPrivateKey(pathKey.privKey);
|
||||
};
|
||||
|
||||
const hideShowPKDialog = () => {
|
||||
setShowPKDialog(false);
|
||||
setPrivateKey(undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={styles.signLink}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
setShowPKDialog(true);
|
||||
}}>
|
||||
<Text
|
||||
variant="titleSmall"
|
||||
style={[styles.hyperlink, { color: theme.colors.primary }]}>
|
||||
Show Private Key
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View>
|
||||
<Dialog open={showPKDialog} onClose={hideShowPKDialog}>
|
||||
<DialogTitle>
|
||||
{!privateKey ? (
|
||||
<Typography>Show Private Key?</Typography>
|
||||
) : (
|
||||
<Typography>Private Key</Typography>
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
{privateKey && (
|
||||
<View style={[styles.dataBox, styles.dataBoxContainer]}>
|
||||
<Typography
|
||||
component="pre"
|
||||
variant="body1"
|
||||
style={styles.dataBoxData}
|
||||
>
|
||||
{privateKey}
|
||||
</Typography>
|
||||
</View>
|
||||
)}
|
||||
<View>
|
||||
<Typography variant="body1" style={styles.dialogWarning}>
|
||||
<Typography component="span">
|
||||
Warning:
|
||||
</Typography>
|
||||
Never disclose this key. Anyone with your private keys can
|
||||
steal any assets held in your account.
|
||||
</Typography>
|
||||
</View>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{!privateKey ? (
|
||||
<>
|
||||
<Button onClick={handleShowPrivateKey} color="error">
|
||||
Yes
|
||||
</Button>
|
||||
<Button onClick={hideShowPKDialog}>No</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button onClick={hideShowPKDialog}>Ok</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShowPKDialog;
|
28
src/components/TxErrorDialog.tsx
Normal file
28
src/components/TxErrorDialog.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { Button, Dialog, Portal, Text } from 'react-native-paper';
|
||||
|
||||
const TxErrorDialog = ({
|
||||
error,
|
||||
visible,
|
||||
hideDialog,
|
||||
}: {
|
||||
error: string;
|
||||
visible: boolean;
|
||||
hideDialog: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<Portal>
|
||||
<Dialog visible={visible} onDismiss={hideDialog}>
|
||||
<Dialog.Title>Transaction Error</Dialog.Title>
|
||||
<Dialog.Content>
|
||||
<Text variant="bodyMedium">{error}</Text>
|
||||
</Dialog.Content>
|
||||
<Dialog.Actions>
|
||||
<Button onPress={hideDialog}>OK</Button>
|
||||
</Dialog.Actions>
|
||||
</Dialog>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TxErrorDialog;
|
39
src/context/AccountsContext.tsx
Normal file
39
src/context/AccountsContext.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import React, { createContext, useContext, useState } from 'react';
|
||||
|
||||
import { Account } from '../types';
|
||||
|
||||
const AccountsContext = createContext<{
|
||||
accounts: Account[];
|
||||
setAccounts: (account: Account[]) => void;
|
||||
currentIndex: number;
|
||||
setCurrentIndex: (index: number) => void;
|
||||
}>({
|
||||
accounts: [],
|
||||
setAccounts: () => {},
|
||||
currentIndex: 0,
|
||||
setCurrentIndex: () => {},
|
||||
});
|
||||
|
||||
const useAccounts = () => {
|
||||
const accountsContext = useContext(AccountsContext);
|
||||
return accountsContext;
|
||||
};
|
||||
|
||||
const AccountsProvider = ({ children }: { children: any }) => {
|
||||
const [accounts, setAccounts] = useState<Account[]>([]);
|
||||
const [currentIndex, setCurrentIndex] = useState<number>(0);
|
||||
|
||||
return (
|
||||
<AccountsContext.Provider
|
||||
value={{
|
||||
accounts,
|
||||
setAccounts,
|
||||
currentIndex,
|
||||
setCurrentIndex,
|
||||
}}>
|
||||
{children}
|
||||
</AccountsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export { useAccounts, AccountsProvider };
|
76
src/context/NetworksContext.tsx
Normal file
76
src/context/NetworksContext.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { NetworksDataState } from '../types';
|
||||
import { retrieveNetworksData, storeNetworkData } from '../utils/accounts';
|
||||
import { DEFAULT_NETWORKS, EIP155 } from '../utils/constants';
|
||||
|
||||
const NetworksContext = createContext<{
|
||||
networksData: NetworksDataState[];
|
||||
setNetworksData: React.Dispatch<React.SetStateAction<NetworksDataState[]>>;
|
||||
networkType: string;
|
||||
setNetworkType: (networkType: string) => void;
|
||||
selectedNetwork?: NetworksDataState;
|
||||
setSelectedNetwork: React.Dispatch<
|
||||
React.SetStateAction<NetworksDataState | undefined>
|
||||
>;
|
||||
}>({
|
||||
networksData: [],
|
||||
setNetworksData: () => {},
|
||||
networkType: '',
|
||||
setNetworkType: () => {},
|
||||
selectedNetwork: {} as NetworksDataState,
|
||||
setSelectedNetwork: () => {},
|
||||
});
|
||||
|
||||
const useNetworks = () => {
|
||||
const networksContext = useContext(NetworksContext);
|
||||
return networksContext;
|
||||
};
|
||||
|
||||
const NetworksProvider = ({ children }: { children: any }) => {
|
||||
const [networksData, setNetworksData] = useState<NetworksDataState[]>([]);
|
||||
const [networkType, setNetworkType] = useState<string>(EIP155);
|
||||
const [selectedNetwork, setSelectedNetwork] = useState<NetworksDataState>();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const retrievedNetworks = await retrieveNetworksData();
|
||||
if (retrievedNetworks.length === 0) {
|
||||
for (const defaultNetwork of DEFAULT_NETWORKS) {
|
||||
await storeNetworkData(defaultNetwork);
|
||||
}
|
||||
}
|
||||
const retrievedNewNetworks = await retrieveNetworksData();
|
||||
setNetworksData(retrievedNewNetworks);
|
||||
setSelectedNetwork(retrievedNewNetworks[0]);
|
||||
};
|
||||
|
||||
if (networksData.length === 0) {
|
||||
fetchData();
|
||||
}
|
||||
}, [networksData]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedNetwork(prevSelectedNetwork => {
|
||||
return networksData.find(
|
||||
networkData => networkData.networkId === prevSelectedNetwork?.networkId,
|
||||
);
|
||||
});
|
||||
}, [networksData]);
|
||||
|
||||
return (
|
||||
<NetworksContext.Provider
|
||||
value={{
|
||||
networksData,
|
||||
setNetworksData,
|
||||
networkType,
|
||||
setNetworkType,
|
||||
selectedNetwork,
|
||||
setSelectedNetwork,
|
||||
}}>
|
||||
{children}
|
||||
</NetworksContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export { useNetworks, NetworksProvider };
|
43
src/context/WalletConnectContext.tsx
Normal file
43
src/context/WalletConnectContext.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
// import { SessionTypes } from '@walletconnect/types';
|
||||
|
||||
import { WalletConnectContextProps } from '../types';
|
||||
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
|
||||
import useInitialization from '../hooks/useInitialization';
|
||||
|
||||
const WalletConnectContext = createContext<WalletConnectContextProps>({
|
||||
activeSessions: {},
|
||||
setActiveSessions: () => {},
|
||||
});
|
||||
|
||||
const useWalletConnect = () => {
|
||||
const walletConnectContext = useContext(WalletConnectContext);
|
||||
return walletConnectContext;
|
||||
};
|
||||
|
||||
const WalletConnectProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
useInitialization();
|
||||
|
||||
useEffect(() => {
|
||||
const sessions = (web3wallet && web3wallet.getActiveSessions()) || {};
|
||||
setActiveSessions(sessions);
|
||||
}, []);
|
||||
|
||||
const [activeSessions, setActiveSessions] = useState<
|
||||
// Record<string, SessionTypes.Struct>
|
||||
Record<string, any>
|
||||
>({});
|
||||
|
||||
return (
|
||||
<WalletConnectContext.Provider
|
||||
value={{
|
||||
activeSessions,
|
||||
setActiveSessions,
|
||||
}}>
|
||||
{children}
|
||||
</WalletConnectContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export { useWalletConnect, WalletConnectProvider };
|
23
src/hooks/useInitialization.ts
Normal file
23
src/hooks/useInitialization.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { createWeb3Wallet } from '../utils/wallet-connect/WalletConnectUtils';
|
||||
|
||||
export default function useInitialization() {
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
|
||||
const onInitialize = useCallback(async () => {
|
||||
try {
|
||||
await createWeb3Wallet();
|
||||
setInitialized(true);
|
||||
} catch (err: unknown) {
|
||||
console.error('Error for initializing', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialized) {
|
||||
onInitialize();
|
||||
}
|
||||
}, [initialized, onInitialize]);
|
||||
|
||||
return initialized;
|
||||
}
|
10
src/hooks/usePrevious.ts
Normal file
10
src/hooks/usePrevious.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export function usePrevious<T>(value: T): T | undefined {
|
||||
const ref = useRef(value);
|
||||
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
}, [value]);
|
||||
return ref.current;
|
||||
}
|
13
src/index.css
Normal file
13
src/index.css
Normal file
@ -0,0 +1,13 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
81
src/index.tsx
Normal file
81
src/index.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { PaperProvider, MD3LightTheme as DefaultTheme, } from 'react-native-paper';
|
||||
import { NavigationContainer } from '@react-navigation/native';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import { AccountsProvider } from './context/AccountsContext';
|
||||
import { NetworksProvider } from './context/NetworksContext';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import { WalletConnectProvider } from './context/WalletConnectContext';
|
||||
|
||||
// // Generate the required CSS
|
||||
// import iconFont from 'react-native-vector-icons/Fonts/FontAwesome.ttf';
|
||||
// const iconFontStyles = `@font-face {
|
||||
// src: url(${iconFont});
|
||||
// font-family: FontAwesome;
|
||||
// }`;
|
||||
|
||||
// // Create a stylesheet
|
||||
// const style = document.createElement('style');
|
||||
// style.type = 'text/css';
|
||||
|
||||
// // Append the iconFontStyles to the stylesheet
|
||||
// if (style.styleSheet) {
|
||||
// style.styleSheet.cssText = iconFontStyles;
|
||||
// } else {
|
||||
// style.appendChild(document.createTextNode(iconFontStyles));
|
||||
// }
|
||||
|
||||
// // Inject the stylesheet into the document head
|
||||
// document.head.appendChild(style);
|
||||
|
||||
const linking = {
|
||||
prefixes: ['https://wallet.laconic.com'],
|
||||
config: {
|
||||
screens: {
|
||||
SignRequest: {
|
||||
path: 'sign/:namespace/:chaindId/:address/:message',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const theme = {
|
||||
...DefaultTheme,
|
||||
dark: false,
|
||||
};
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
root.render(
|
||||
<PaperProvider theme={theme}>
|
||||
<NetworksProvider>
|
||||
<AccountsProvider>
|
||||
<WalletConnectProvider>
|
||||
<NavigationContainer linking={linking}>
|
||||
<React.Fragment>
|
||||
{Platform.OS === 'web' ? (
|
||||
<style type="text/css">{`
|
||||
@font-face {
|
||||
font-family: 'MaterialCommunityIcons';
|
||||
src: url(${require('react-native-vector-icons/Fonts/MaterialCommunityIcons.ttf')}) format('truetype');
|
||||
}
|
||||
`}</style>
|
||||
) : null}
|
||||
<App />
|
||||
</React.Fragment>
|
||||
</NavigationContainer>
|
||||
</WalletConnectProvider>
|
||||
</AccountsProvider>
|
||||
</NetworksProvider>
|
||||
</PaperProvider>
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
1
src/logo.svg
Normal file
1
src/logo.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
After Width: | Height: | Size: 2.6 KiB |
1
src/react-app-env.d.ts
vendored
Normal file
1
src/react-app-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
15
src/reportWebVitals.ts
Normal file
15
src/reportWebVitals.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { ReportHandler } from 'web-vitals';
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
418
src/screens/AddNetwork.tsx
Normal file
418
src/screens/AddNetwork.tsx
Normal file
@ -0,0 +1,418 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { useForm, Controller, useWatch, FieldErrors } from 'react-hook-form';
|
||||
import { TextInput, Button, 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';
|
||||
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import { StackParamsList } from '../types';
|
||||
import { SelectNetworkType } from '../components/SelectNetworkType';
|
||||
import { storeNetworkData } 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 { getCosmosAccounts } from '../utils/accounts';
|
||||
import ETH_CHAINS from '../assets/ethereum-chains.json';
|
||||
import {
|
||||
getInternetCredentials,
|
||||
setInternetCredentials,
|
||||
} from '../utils/key-store';
|
||||
|
||||
const ethNetworkDataSchema = z.object({
|
||||
chainId: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
|
||||
networkName: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
|
||||
rpcUrl: z.string().url({ message: INVALID_URL_ERROR }),
|
||||
blockExplorerUrl: z
|
||||
.string()
|
||||
.url({ message: INVALID_URL_ERROR })
|
||||
.or(z.literal('')),
|
||||
coinType: z
|
||||
.string()
|
||||
.nonempty({ message: EMPTY_FIELD_ERROR })
|
||||
.regex(IS_NUMBER_REGEX),
|
||||
currencySymbol: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
|
||||
});
|
||||
|
||||
const cosmosNetworkDataSchema = z.object({
|
||||
chainId: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
|
||||
networkName: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
|
||||
rpcUrl: z.string().url({ message: INVALID_URL_ERROR }),
|
||||
blockExplorerUrl: z
|
||||
.string()
|
||||
.url({ message: INVALID_URL_ERROR })
|
||||
.or(z.literal('')),
|
||||
coinType: z
|
||||
.string()
|
||||
.nonempty({ message: EMPTY_FIELD_ERROR })
|
||||
.regex(IS_NUMBER_REGEX),
|
||||
nativeDenom: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
|
||||
addressPrefix: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
|
||||
gasPrice: z
|
||||
.string()
|
||||
.nonempty({ message: EMPTY_FIELD_ERROR })
|
||||
.regex(/^\d+(\.\d+)?$/),
|
||||
});
|
||||
|
||||
const AddNetwork = () => {
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<StackParamsList>>();
|
||||
|
||||
const { setNetworksData } = useNetworks();
|
||||
|
||||
const [namespace, setNamespace] = useState<string>(EIP155);
|
||||
|
||||
const networksFormDataSchema =
|
||||
namespace === EIP155 ? ethNetworkDataSchema : cosmosNetworkDataSchema;
|
||||
|
||||
const {
|
||||
control,
|
||||
formState: { errors, isSubmitting },
|
||||
handleSubmit,
|
||||
setValue,
|
||||
reset,
|
||||
} = useForm<z.infer<typeof networksFormDataSchema>>({
|
||||
mode: 'onChange',
|
||||
resolver: zodResolver(networksFormDataSchema),
|
||||
});
|
||||
|
||||
const watchChainId = useWatch({
|
||||
control,
|
||||
name: 'chainId',
|
||||
});
|
||||
|
||||
const updateNetworkType = (newNetworkType: string) => {
|
||||
setNamespace(newNetworkType);
|
||||
};
|
||||
|
||||
const fetchChainDetails = useDebouncedCallback((chainId: string) => {
|
||||
if (namespace === EIP155) {
|
||||
const ethChainDetails = ETH_CHAINS.find(
|
||||
chain => chain.chainId === Number(chainId),
|
||||
);
|
||||
if (!ethChainDetails) {
|
||||
return;
|
||||
}
|
||||
setValue('networkName', ethChainDetails.name);
|
||||
setValue('rpcUrl', ethChainDetails.rpc[0]);
|
||||
setValue('blockExplorerUrl', ethChainDetails.explorers?.[0].url || '');
|
||||
setValue('coinType', String(ethChainDetails.slip44 ?? '60'));
|
||||
setValue('currencySymbol', ethChainDetails.nativeCurrency.symbol);
|
||||
return;
|
||||
}
|
||||
const cosmosChainDetails = chains.find(
|
||||
({ chain_id }) => chain_id === chainId,
|
||||
);
|
||||
if (!cosmosChainDetails) {
|
||||
return;
|
||||
}
|
||||
setValue('networkName', cosmosChainDetails.pretty_name);
|
||||
setValue('rpcUrl', cosmosChainDetails.apis?.rpc?.[0]?.address || '');
|
||||
setValue('blockExplorerUrl', cosmosChainDetails.explorers?.[0].url || '');
|
||||
setValue('addressPrefix', cosmosChainDetails.bech32_prefix);
|
||||
setValue('coinType', String(cosmosChainDetails.slip44 ?? '118'));
|
||||
setValue('nativeDenom', cosmosChainDetails.fees?.fee_tokens[0].denom || '');
|
||||
setValue(
|
||||
'gasPrice',
|
||||
String(
|
||||
cosmosChainDetails.fees?.fee_tokens[0].average_gas_price ||
|
||||
String(process.env.DEFAULT_GAS_PRICE),
|
||||
),
|
||||
);
|
||||
}, CHAINID_DEBOUNCE_DELAY);
|
||||
|
||||
const submit = useCallback(
|
||||
async (data: z.infer<typeof networksFormDataSchema>) => {
|
||||
const newNetworkData = {
|
||||
...data,
|
||||
namespace,
|
||||
isDefault: false,
|
||||
};
|
||||
|
||||
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 getCosmosAccounts(
|
||||
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);
|
||||
|
||||
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',
|
||||
),
|
||||
]);
|
||||
|
||||
navigation.navigate('Laconic');
|
||||
},
|
||||
[navigation, namespace, setNetworksData],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchChainDetails(watchChainId);
|
||||
}, [watchChainId, fetchChainDetails]);
|
||||
|
||||
useEffect(() => {
|
||||
reset();
|
||||
}, [namespace, reset]);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<SelectNetworkType updateNetworkType={updateNetworkType} />
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="chainId"
|
||||
defaultValue=""
|
||||
render={({ field: { onChange, onBlur, value } }) => (
|
||||
<>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={value}
|
||||
label="Chain ID"
|
||||
onBlur={onBlur}
|
||||
onChangeText={textValue => onChange(textValue)}
|
||||
/>
|
||||
<HelperText type="error">{errors.chainId?.message}</HelperText>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="networkName"
|
||||
render={({ field: { onChange, onBlur, value } }) => (
|
||||
<>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
label="Network Name"
|
||||
value={value}
|
||||
onBlur={onBlur}
|
||||
onChangeText={textValue => onChange(textValue)}
|
||||
/>
|
||||
<HelperText type="error">{errors.networkName?.message}</HelperText>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="rpcUrl"
|
||||
defaultValue=""
|
||||
render={({ field: { onChange, onBlur, value } }) => (
|
||||
<>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
label="New RPC URL"
|
||||
onBlur={onBlur}
|
||||
value={value}
|
||||
onChangeText={textValue => onChange(textValue)}
|
||||
/>
|
||||
<HelperText type="error">{errors.rpcUrl?.message}</HelperText>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="blockExplorerUrl"
|
||||
render={({ field: { onChange, onBlur, value } }) => (
|
||||
<>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={value}
|
||||
label="Block Explorer URL (Optional)"
|
||||
onBlur={onBlur}
|
||||
onChangeText={textValue => onChange(textValue)}
|
||||
/>
|
||||
<HelperText type="error">
|
||||
{errors.blockExplorerUrl?.message}
|
||||
</HelperText>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="coinType"
|
||||
defaultValue=""
|
||||
render={({ field: { onChange, onBlur, value } }) => (
|
||||
<>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={value}
|
||||
label="Coin Type"
|
||||
onBlur={onBlur}
|
||||
onChangeText={onChange}
|
||||
/>
|
||||
<HelperText type="error">{errors.coinType?.message}</HelperText>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
{namespace === EIP155 ? (
|
||||
<Controller
|
||||
control={control}
|
||||
name="currencySymbol"
|
||||
defaultValue=""
|
||||
render={({ field: { onChange, onBlur, value } }) => (
|
||||
<>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={value}
|
||||
label="Currency Symbol"
|
||||
onBlur={onBlur}
|
||||
onChangeText={textValue => onChange(textValue)}
|
||||
/>
|
||||
<HelperText type="error">
|
||||
{
|
||||
(errors as FieldErrors<z.infer<typeof ethNetworkDataSchema>>)
|
||||
.currencySymbol?.message
|
||||
}
|
||||
</HelperText>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
name="nativeDenom"
|
||||
defaultValue=""
|
||||
render={({ field: { onChange, onBlur, value } }) => (
|
||||
<>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={value}
|
||||
label="Native Denom"
|
||||
onBlur={onBlur}
|
||||
onChangeText={textValue => onChange(textValue)}
|
||||
/>
|
||||
<HelperText type="error">
|
||||
{
|
||||
(
|
||||
errors as FieldErrors<
|
||||
z.infer<typeof cosmosNetworkDataSchema>
|
||||
>
|
||||
).nativeDenom?.message
|
||||
}
|
||||
</HelperText>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="addressPrefix"
|
||||
defaultValue=""
|
||||
render={({ field: { onChange, onBlur, value } }) => (
|
||||
<>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={value}
|
||||
label="Address Prefix"
|
||||
onBlur={onBlur}
|
||||
onChangeText={textValue => onChange(textValue)}
|
||||
/>
|
||||
<HelperText type="error">
|
||||
{
|
||||
(
|
||||
errors as FieldErrors<
|
||||
z.infer<typeof cosmosNetworkDataSchema>
|
||||
>
|
||||
).addressPrefix?.message
|
||||
}
|
||||
</HelperText>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="gasPrice"
|
||||
defaultValue=""
|
||||
render={({ field: { onChange, onBlur, value } }) => (
|
||||
<>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={value}
|
||||
label="Gas Price"
|
||||
onBlur={onBlur}
|
||||
onChangeText={onChange}
|
||||
/>
|
||||
<HelperText type="error">
|
||||
{
|
||||
(
|
||||
errors as FieldErrors<
|
||||
z.infer<typeof cosmosNetworkDataSchema>
|
||||
>
|
||||
).gasPrice?.message
|
||||
}
|
||||
</HelperText>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
mode="contained"
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
onPress={handleSubmit(submit)}>
|
||||
{isSubmitting ? 'Adding' : 'Submit'}
|
||||
</Button>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddNetwork;
|
120
src/screens/AddSession.tsx
Normal file
120
src/screens/AddSession.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { AppState, TouchableOpacity, View } from 'react-native';
|
||||
import { Button, Text, TextInput } from 'react-native-paper';
|
||||
// import {
|
||||
// Camera,
|
||||
// useCameraDevice,
|
||||
// useCameraPermission,
|
||||
// useCodeScanner,
|
||||
// } from 'react-native-vision-camera';
|
||||
import { Linking } from 'react-native';
|
||||
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
|
||||
import { web3WalletPair } from '../utils/wallet-connect/WalletConnectUtils';
|
||||
import styles from '../styles/stylesheet';
|
||||
import { StackParamsList } from '../types';
|
||||
|
||||
const AddSession = () => {
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<StackParamsList>>();
|
||||
|
||||
// const { hasPermission, requestPermission } = useCameraPermission();
|
||||
// const device = useCameraDevice('back');
|
||||
|
||||
const [currentWCURI, setCurrentWCURI] = useState<string>('');
|
||||
// const [isActive, setIsActive] = useState(AppState.currentState === 'active');
|
||||
// const [isScanning, setScanning] = useState(true);
|
||||
|
||||
// const codeScanner = useCodeScanner({
|
||||
// codeTypes: ['qr'],
|
||||
// onCodeScanned: codes => {
|
||||
// if (isScanning) {
|
||||
// codes.forEach(code => {
|
||||
// if (code.value) {
|
||||
// setCurrentWCURI(code.value);
|
||||
// setScanning(false);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// },
|
||||
// });
|
||||
|
||||
const linkToSettings = async () => {
|
||||
await Linking.openSettings();
|
||||
};
|
||||
|
||||
const pair = async () => {
|
||||
const pairing = await web3WalletPair({ uri: currentWCURI });
|
||||
navigation.navigate('WalletConnect');
|
||||
return pairing;
|
||||
};
|
||||
|
||||
// useEffect(() => {
|
||||
// const handleAppStateChange = (newState: string) => {
|
||||
// setIsActive(newState === 'active');
|
||||
// };
|
||||
|
||||
// AppState.addEventListener('change', handleAppStateChange);
|
||||
|
||||
// if (!hasPermission) {
|
||||
// requestPermission();
|
||||
// }
|
||||
// }, [hasPermission, requestPermission]);
|
||||
|
||||
return (
|
||||
<View style={styles.appContainer}>
|
||||
{/* {!hasPermission || !device ? ( */}
|
||||
{false ? (
|
||||
<>
|
||||
{/* <Text>
|
||||
{!hasPermission
|
||||
? 'No Camera Permission granted'
|
||||
: 'No Camera Selected'}
|
||||
</Text> */}
|
||||
<TouchableOpacity onPress={linkToSettings}>
|
||||
<Text variant="titleSmall" style={[styles.hyperlink]}>
|
||||
Go to settings
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<View style={styles.cameraContainer}>
|
||||
{/* {isActive ? (
|
||||
<Camera
|
||||
style={styles.camera}
|
||||
device={device}
|
||||
isActive={isActive}
|
||||
codeScanner={codeScanner}
|
||||
video={false}
|
||||
/>
|
||||
) : (
|
||||
<Text>No Camera Selected!</Text>
|
||||
)} */}
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<Text variant="titleMedium">Enter WalletConnect URI</Text>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
onChangeText={setCurrentWCURI}
|
||||
value={currentWCURI}
|
||||
numberOfLines={4}
|
||||
multiline={true}
|
||||
style={styles.walletConnectUriText}
|
||||
/>
|
||||
|
||||
<View style={styles.signButton}>
|
||||
<Button mode="contained" onPress={pair}>
|
||||
Pair Session
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
export default AddSession;
|
300
src/screens/ApproveTransaction.tsx
Normal file
300
src/screens/ApproveTransaction.tsx
Normal file
@ -0,0 +1,300 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Image, ScrollView, View } from 'react-native';
|
||||
import { Button, Text, TextInput } from 'react-native-paper';
|
||||
import { SvgUri } from 'react-native-svg';
|
||||
import Config from 'react-native-config';
|
||||
|
||||
import {
|
||||
NativeStackNavigationProp,
|
||||
NativeStackScreenProps,
|
||||
} from '@react-navigation/native-stack';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { DirectSecp256k1Wallet } from '@cosmjs/proto-signing';
|
||||
import { LaconicClient } from '@cerc-io/registry-sdk';
|
||||
import { GasPrice, calculateFee } from '@cosmjs/stargate';
|
||||
import { formatJsonRpcError } from '@json-rpc-tools/utils';
|
||||
|
||||
import { useNetworks } from '../context/NetworksContext';
|
||||
import { Account, StackParamsList } from '../types';
|
||||
import styles from '../styles/stylesheet';
|
||||
import { COSMOS, IS_NUMBER_REGEX } from '../utils/constants';
|
||||
import { retrieveSingleAccount } from '../utils/accounts';
|
||||
import { getPathKey } from '../utils/misc';
|
||||
import {
|
||||
WalletConnectRequests,
|
||||
approveWalletConnectRequest,
|
||||
rejectWalletConnectRequest,
|
||||
} from '../utils/wallet-connect/wallet-connect-requests';
|
||||
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
|
||||
import { MEMO } from './ApproveTransfer';
|
||||
import TxErrorDialog from '../components/TxErrorDialog';
|
||||
import AccountDetails from '../components/AccountDetails';
|
||||
|
||||
type ApproveTransactionProps = NativeStackScreenProps<
|
||||
StackParamsList,
|
||||
'ApproveTransaction'
|
||||
>;
|
||||
|
||||
const ApproveTransaction = ({ route }: ApproveTransactionProps) => {
|
||||
const { networksData } = useNetworks();
|
||||
|
||||
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 transactionMessage = route.params.transactionMessage;
|
||||
const signer = route.params.signer;
|
||||
const requestEvent = route.params.requestEvent;
|
||||
const chainId = requestEvent.params.chainId;
|
||||
const requestEventId = requestEvent.id;
|
||||
const topic = requestEvent.topic;
|
||||
|
||||
const [account, setAccount] = useState<Account>();
|
||||
const [cosmosStargateClient, setCosmosStargateClient] =
|
||||
useState<LaconicClient>();
|
||||
const [cosmosGasLimit, setCosmosGasLimit] = useState<string>();
|
||||
const [fees, setFees] = useState<string>();
|
||||
const [txError, setTxError] = useState<string>();
|
||||
const [isTxErrorDialogOpen, setIsTxErrorDialogOpen] = useState(false);
|
||||
const [isRequestAccepted, setIsRequestAccepted] = useState(false);
|
||||
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<StackParamsList>>();
|
||||
|
||||
const requestedNetwork = networksData.find(
|
||||
networkData =>
|
||||
`${networkData.namespace}:${networkData.chainId}` === chainId,
|
||||
);
|
||||
const namespace = requestedNetwork!.namespace;
|
||||
|
||||
useEffect(() => {
|
||||
if (namespace !== COSMOS) {
|
||||
return;
|
||||
}
|
||||
|
||||
const setClient = async () => {
|
||||
if (!account) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cosmosPrivKey = (
|
||||
await getPathKey(
|
||||
`${requestedNetwork?.namespace}:${requestedNetwork?.chainId}`,
|
||||
account.index,
|
||||
)
|
||||
).privKey;
|
||||
|
||||
const sender = await DirectSecp256k1Wallet.fromKey(
|
||||
Buffer.from(cosmosPrivKey.split('0x')[1], 'hex'),
|
||||
requestedNetwork?.addressPrefix,
|
||||
);
|
||||
|
||||
try {
|
||||
const client = await LaconicClient.connectWithSigner(
|
||||
requestedNetwork?.rpcUrl!,
|
||||
sender,
|
||||
);
|
||||
setCosmosStargateClient(client);
|
||||
} catch (error: any) {
|
||||
setTxError(error.message);
|
||||
setIsTxErrorDialogOpen(true);
|
||||
const response = formatJsonRpcError(requestEventId, error.message);
|
||||
await web3wallet!.respondSessionRequest({ topic, response });
|
||||
}
|
||||
};
|
||||
|
||||
setClient();
|
||||
}, [account, requestedNetwork, chainId, namespace, requestEventId, topic]);
|
||||
|
||||
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(() => {
|
||||
retrieveData(signer);
|
||||
}, [retrieveData, signer]);
|
||||
|
||||
useEffect(() => {
|
||||
const getCosmosGas = async () => {
|
||||
try {
|
||||
if (!cosmosStargateClient) {
|
||||
return;
|
||||
}
|
||||
const gasEstimation = await cosmosStargateClient!.simulate(
|
||||
transactionMessage.value.participant!,
|
||||
[transactionMessage],
|
||||
MEMO,
|
||||
);
|
||||
|
||||
setCosmosGasLimit(
|
||||
String(
|
||||
Math.round(gasEstimation * Number(Config.DEFAULT_GAS_ADJUSTMENT)),
|
||||
),
|
||||
);
|
||||
} catch (error: any) {
|
||||
setTxError(error.message);
|
||||
setIsTxErrorDialogOpen(true);
|
||||
const response = formatJsonRpcError(requestEventId, error.message);
|
||||
await web3wallet!.respondSessionRequest({ topic, response });
|
||||
}
|
||||
};
|
||||
getCosmosGas();
|
||||
}, [cosmosStargateClient, transactionMessage, requestEventId, topic]);
|
||||
|
||||
useEffect(() => {
|
||||
const gasPrice = GasPrice.fromString(
|
||||
requestedNetwork?.gasPrice! + requestedNetwork?.nativeDenom,
|
||||
);
|
||||
|
||||
if (!cosmosGasLimit) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cosmosFees = calculateFee(Number(cosmosGasLimit), gasPrice);
|
||||
|
||||
setFees(cosmosFees.amount[0].amount);
|
||||
}, [namespace, cosmosGasLimit, requestedNetwork]);
|
||||
|
||||
const acceptRequestHandler = async () => {
|
||||
try {
|
||||
setIsRequestAccepted(true);
|
||||
if (!account) {
|
||||
throw new Error('account not found');
|
||||
}
|
||||
|
||||
let options: WalletConnectRequests;
|
||||
|
||||
if (!cosmosStargateClient) {
|
||||
throw new Error('Cosmos stargate client not found');
|
||||
}
|
||||
|
||||
options = {
|
||||
type: 'cosmos_sendTransaction',
|
||||
LaconicClient: cosmosStargateClient,
|
||||
// StdFee object
|
||||
cosmosFee: {
|
||||
// This amount is total fees required for transaction
|
||||
amount: [
|
||||
{
|
||||
amount: fees!,
|
||||
denom: requestedNetwork!.nativeDenom!,
|
||||
},
|
||||
],
|
||||
gas: cosmosGasLimit!,
|
||||
},
|
||||
txMsg: transactionMessage,
|
||||
};
|
||||
|
||||
const response = await approveWalletConnectRequest(
|
||||
requestEvent,
|
||||
account,
|
||||
namespace,
|
||||
requestedNetwork!.chainId,
|
||||
options,
|
||||
);
|
||||
|
||||
await web3wallet!.respondSessionRequest({ topic, response });
|
||||
setIsRequestAccepted(false);
|
||||
navigation.navigate('Laconic');
|
||||
} catch (error: any) {
|
||||
setTxError(error.message);
|
||||
setIsTxErrorDialogOpen(true);
|
||||
const response = formatJsonRpcError(requestEventId, error.message);
|
||||
await web3wallet!.respondSessionRequest({ topic, response });
|
||||
}
|
||||
};
|
||||
|
||||
const rejectRequestHandler = async () => {
|
||||
const response = rejectWalletConnectRequest(requestEvent);
|
||||
await web3wallet!.respondSessionRequest({
|
||||
topic,
|
||||
response,
|
||||
});
|
||||
|
||||
navigation.navigate('Laconic');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScrollView contentContainerStyle={styles.approveTransaction}>
|
||||
<View style={styles.dappDetails}>
|
||||
{requestIcon && (
|
||||
<>
|
||||
{requestIcon.endsWith('.svg') ? (
|
||||
<View style={styles.dappLogo}>
|
||||
<SvgUri height="50" width="50" uri={requestIcon} />
|
||||
</View>
|
||||
) : (
|
||||
<Image style={styles.dappLogo} source={{ uri: requestIcon }} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Text>{requestName}</Text>
|
||||
<Text variant="bodySmall">{requestURL}</Text>
|
||||
</View>
|
||||
<AccountDetails account={account} />
|
||||
<Text variant="bodyLarge" style={styles.transactionLabel}>
|
||||
Message:
|
||||
</Text>
|
||||
<View style={styles.messageBody}>
|
||||
<Text variant="bodyLarge">
|
||||
{JSON.stringify(transactionMessage, null, 2)}
|
||||
</Text>
|
||||
</View>
|
||||
<>
|
||||
<Text variant="bodyLarge" style={styles.transactionLabel}>
|
||||
Gas Limit:
|
||||
</Text>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
style={styles.transactionFeesInput}
|
||||
value={cosmosGasLimit}
|
||||
onChangeText={value => {
|
||||
if (IS_NUMBER_REGEX.test(value)) {
|
||||
setCosmosGasLimit(value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<View style={styles.buttonContainer}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={acceptRequestHandler}
|
||||
loading={isRequestAccepted}
|
||||
disabled={isRequestAccepted}>
|
||||
Yes
|
||||
</Button>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={rejectRequestHandler}
|
||||
buttonColor="#B82B0D">
|
||||
No
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
</ScrollView>
|
||||
<TxErrorDialog
|
||||
error={txError!}
|
||||
visible={isTxErrorDialogOpen}
|
||||
hideDialog={async () => {
|
||||
setIsTxErrorDialogOpen(false);
|
||||
navigation.navigate('Laconic');
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApproveTransaction;
|
648
src/screens/ApproveTransfer.tsx
Normal file
648
src/screens/ApproveTransfer.tsx
Normal file
@ -0,0 +1,648 @@
|
||||
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 { providers, BigNumber } from 'ethers';
|
||||
import Config from 'react-native-config';
|
||||
import { Deferrable } from 'ethers/lib/utils';
|
||||
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import {
|
||||
NativeStackNavigationProp,
|
||||
NativeStackScreenProps,
|
||||
} from '@react-navigation/native-stack';
|
||||
import { getHeaderTitle } from '@react-navigation/elements';
|
||||
import { DirectSecp256k1Wallet } from '@cosmjs/proto-signing';
|
||||
import {
|
||||
calculateFee,
|
||||
GasPrice,
|
||||
MsgSendEncodeObject,
|
||||
SigningStargateClient,
|
||||
} from '@cosmjs/stargate';
|
||||
|
||||
import { Account, StackParamsList } from '../types';
|
||||
import AccountDetails from '../components/AccountDetails';
|
||||
import styles from '../styles/stylesheet';
|
||||
import { retrieveSingleAccount } from '../utils/accounts';
|
||||
import {
|
||||
approveWalletConnectRequest,
|
||||
rejectWalletConnectRequest,
|
||||
WalletConnectRequests,
|
||||
} from '../utils/wallet-connect/wallet-connect-requests';
|
||||
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
|
||||
import DataBox from '../components/DataBox';
|
||||
import { getPathKey } from '../utils/misc';
|
||||
import { useNetworks } from '../context/NetworksContext';
|
||||
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';
|
||||
|
||||
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'
|
||||
>;
|
||||
|
||||
const ApproveTransfer = ({ route }: SignRequestProps) => {
|
||||
const { networksData } = useNetworks();
|
||||
|
||||
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<Account>();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [balance, setBalance] = useState<string>('');
|
||||
const [isTxLoading, setIsTxLoading] = useState(false);
|
||||
const [cosmosStargateClient, setCosmosStargateClient] =
|
||||
useState<SigningStargateClient>();
|
||||
const [fees, setFees] = useState<string>();
|
||||
const [cosmosGasLimit, setCosmosGasLimit] = useState<string>();
|
||||
const [txError, setTxError] = useState<string>();
|
||||
const [isTxErrorDialogOpen, setIsTxErrorDialogOpen] = useState(false);
|
||||
const [ethGasPrice, setEthGasPrice] = useState<BigNumber | null>();
|
||||
const [ethGasLimit, setEthGasLimit] = useState<BigNumber>();
|
||||
const [ethMaxFee, setEthMaxFee] = useState<BigNumber | null>();
|
||||
const [ethMaxPriorityFee, setEthMaxPriorityFee] =
|
||||
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;
|
||||
}
|
||||
}, [balance, transaction]);
|
||||
|
||||
const requestedNetwork = networksData.find(
|
||||
networkData =>
|
||||
`${networkData.namespace}:${networkData.chainId}` === chainId,
|
||||
);
|
||||
const namespace = requestedNetwork!.namespace;
|
||||
|
||||
const sendMsg: MsgSendEncodeObject = useMemo(() => {
|
||||
return {
|
||||
typeUrl: '/cosmos.bank.v1beta1.MsgSend',
|
||||
value: {
|
||||
fromAddress: transaction.from,
|
||||
toAddress: transaction.to,
|
||||
amount: [
|
||||
{
|
||||
amount: String(transaction.value),
|
||||
denom: requestedNetwork!.nativeDenom!,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}, [requestedNetwork, transaction]);
|
||||
|
||||
useEffect(() => {
|
||||
if (namespace !== COSMOS) {
|
||||
return;
|
||||
}
|
||||
|
||||
const setClient = async () => {
|
||||
if (!account) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cosmosPrivKey = (
|
||||
await getPathKey(
|
||||
`${requestedNetwork?.namespace}:${requestedNetwork?.chainId}`,
|
||||
account.index,
|
||||
)
|
||||
).privKey;
|
||||
|
||||
const sender = await DirectSecp256k1Wallet.fromKey(
|
||||
Buffer.from(cosmosPrivKey.split('0x')[1], 'hex'),
|
||||
requestedNetwork?.addressPrefix,
|
||||
);
|
||||
|
||||
try {
|
||||
const client = await SigningStargateClient.connectWithSigner(
|
||||
requestedNetwork?.rpcUrl!,
|
||||
sender,
|
||||
);
|
||||
|
||||
setCosmosStargateClient(client);
|
||||
} catch (error: any) {
|
||||
setTxError(error.message);
|
||||
setIsTxErrorDialogOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
setClient();
|
||||
}, [account, requestedNetwork, chainId, namespace]);
|
||||
|
||||
const provider = useMemo(() => {
|
||||
if (namespace === EIP155) {
|
||||
if (!requestedNetwork) {
|
||||
throw new Error('Requested chain not supported');
|
||||
}
|
||||
try {
|
||||
const ethProvider = new providers.JsonRpcProvider(
|
||||
requestedNetwork.rpcUrl,
|
||||
);
|
||||
|
||||
return ethProvider;
|
||||
} catch (error: any) {
|
||||
setTxError(error.message);
|
||||
setIsTxErrorDialogOpen(true);
|
||||
}
|
||||
}
|
||||
}, [requestedNetwork, namespace]);
|
||||
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<StackParamsList>>();
|
||||
|
||||
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
|
||||
if (
|
||||
// If requested chain is EVM compatible, set loading to false when ethMaxFee and ethPriorityFee have been populated
|
||||
(ethMaxFee !== undefined && ethMaxPriorityFee !== undefined) ||
|
||||
// Or if requested chain is a cosmos chain, set loading to false when cosmosGasLimit has been populated
|
||||
cosmosGasLimit !== undefined
|
||||
) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [ethMaxFee, ethMaxPriorityFee, cosmosGasLimit]);
|
||||
|
||||
useEffect(() => {
|
||||
if (namespace === EIP155) {
|
||||
const ethFees = BigNumber.from(ethGasLimit ?? 0)
|
||||
.mul(BigNumber.from(ethMaxFee ?? ethGasPrice ?? 0))
|
||||
.toString();
|
||||
setFees(ethFees);
|
||||
} else {
|
||||
const gasPrice = GasPrice.fromString(
|
||||
requestedNetwork?.gasPrice! + requestedNetwork?.nativeDenom,
|
||||
);
|
||||
|
||||
if (!cosmosGasLimit) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cosmosFees = calculateFee(Number(cosmosGasLimit), gasPrice);
|
||||
|
||||
setFees(cosmosFees.amount[0].amount);
|
||||
}
|
||||
}, [
|
||||
transaction,
|
||||
namespace,
|
||||
ethGasLimit,
|
||||
ethGasPrice,
|
||||
cosmosGasLimit,
|
||||
requestedNetwork,
|
||||
ethMaxFee,
|
||||
]);
|
||||
useEffect(() => {
|
||||
retrieveData(transaction.from!);
|
||||
}, [retrieveData, transaction]);
|
||||
|
||||
const isEIP1559 = useMemo(() => {
|
||||
if (cosmosGasLimit) {
|
||||
return;
|
||||
}
|
||||
if (ethMaxFee !== null && ethMaxPriorityFee !== null) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, [cosmosGasLimit, ethMaxFee, ethMaxPriorityFee]);
|
||||
|
||||
const acceptRequestHandler = async () => {
|
||||
setIsTxLoading(true);
|
||||
try {
|
||||
if (!account) {
|
||||
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 (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,
|
||||
// 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('Laconic');
|
||||
} catch (error: any) {
|
||||
setTxError(error.message);
|
||||
setIsTxErrorDialogOpen(true);
|
||||
}
|
||||
setIsTxLoading(false);
|
||||
};
|
||||
|
||||
const rejectRequestHandler = async () => {
|
||||
const response = rejectWalletConnectRequest(requestEvent);
|
||||
const { topic } = requestEvent;
|
||||
await web3wallet!.respondSessionRequest({
|
||||
topic,
|
||||
response,
|
||||
});
|
||||
|
||||
navigation.navigate('Laconic');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const getAccountBalance = async () => {
|
||||
try {
|
||||
if (!account) {
|
||||
return;
|
||||
}
|
||||
if (namespace === EIP155) {
|
||||
if (!provider) {
|
||||
return;
|
||||
}
|
||||
const fetchedBalance = await provider.getBalance(account.address);
|
||||
setBalance(fetchedBalance ? fetchedBalance.toString() : '0');
|
||||
} else {
|
||||
const cosmosBalance = await cosmosStargateClient?.getBalance(
|
||||
account.address,
|
||||
requestedNetwork!.nativeDenom!.toLowerCase(),
|
||||
);
|
||||
|
||||
setBalance(cosmosBalance?.amount!);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setTxError(error.message);
|
||||
setIsTxErrorDialogOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
getAccountBalance();
|
||||
}, [account, provider, namespace, cosmosStargateClient, requestedNetwork]);
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
header: ({ options, back }) => {
|
||||
const title = getHeaderTitle(options, 'Approve Transaction');
|
||||
|
||||
return (
|
||||
<Appbar.Header>
|
||||
{back && (
|
||||
<Appbar.BackAction
|
||||
onPress={async () => {
|
||||
await rejectRequestHandler();
|
||||
navigation.navigate('Laconic');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Appbar.Content title={title} />
|
||||
</Appbar.Header>
|
||||
);
|
||||
},
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [navigation, route.name]);
|
||||
|
||||
useEffect(() => {
|
||||
const getEthGas = async () => {
|
||||
try {
|
||||
if (!isSufficientFunds || !provider) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await provider.getFeeData();
|
||||
|
||||
setEthMaxFee(data.maxFeePerGas);
|
||||
setEthMaxPriorityFee(data.maxPriorityFeePerGas);
|
||||
setEthGasPrice(data.gasPrice);
|
||||
|
||||
if (transaction.gasLimit) {
|
||||
setEthGasLimit(BigNumber.from(transaction.gasLimit));
|
||||
} else {
|
||||
const transactionObject: Deferrable<providers.TransactionRequest> = {
|
||||
from: transaction.from!,
|
||||
to: transaction.to!,
|
||||
data: transaction.data!,
|
||||
value: transaction.value!,
|
||||
maxFeePerGas: data.maxFeePerGas ?? undefined,
|
||||
maxPriorityFeePerGas: data.maxPriorityFeePerGas ?? undefined,
|
||||
gasPrice: data.maxFeePerGas
|
||||
? undefined
|
||||
: data.gasPrice ?? undefined,
|
||||
};
|
||||
const gasLimit = await provider.estimateGas(transactionObject);
|
||||
setEthGasLimit(gasLimit);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setTxError(error.message);
|
||||
setIsTxErrorDialogOpen(true);
|
||||
}
|
||||
};
|
||||
getEthGas();
|
||||
}, [provider, transaction, isSufficientFunds]);
|
||||
|
||||
useEffect(() => {
|
||||
const getCosmosGas = async () => {
|
||||
try {
|
||||
if (!cosmosStargateClient) {
|
||||
return;
|
||||
}
|
||||
if (!isSufficientFunds) {
|
||||
return;
|
||||
}
|
||||
|
||||
const gasEstimation = await cosmosStargateClient.simulate(
|
||||
transaction.from!,
|
||||
[sendMsg],
|
||||
MEMO,
|
||||
);
|
||||
|
||||
setCosmosGasLimit(
|
||||
String(
|
||||
Math.round(gasEstimation * Number(Config.DEFAULT_GAS_ADJUSTMENT)),
|
||||
),
|
||||
);
|
||||
} catch (error: any) {
|
||||
setTxError(error.message);
|
||||
setIsTxErrorDialogOpen(true);
|
||||
}
|
||||
};
|
||||
getCosmosGas();
|
||||
}, [cosmosStargateClient, isSufficientFunds, sendMsg, transaction]);
|
||||
|
||||
useEffect(() => {
|
||||
if (balance && !isSufficientFunds) {
|
||||
setTxError('Insufficient funds');
|
||||
setIsTxErrorDialogOpen(true);
|
||||
}
|
||||
}, [isSufficientFunds, balance]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<View style={styles.spinnerContainer}>
|
||||
<ActivityIndicator size="large" color="#0000ff" />
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<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>
|
||||
<View style={styles.dataBoxContainer}>
|
||||
<Text style={styles.dataBoxLabel}>From</Text>
|
||||
<View style={styles.dataBox}>
|
||||
<AccountDetails account={account} />
|
||||
</View>
|
||||
</View>
|
||||
<DataBox
|
||||
label={`Balance (${
|
||||
namespace === EIP155 ? 'wei' : requestedNetwork!.nativeDenom
|
||||
})`}
|
||||
data={
|
||||
balance === '' || balance === undefined
|
||||
? 'Loading balance...'
|
||||
: `${balance}`
|
||||
}
|
||||
/>
|
||||
{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()}
|
||||
/>
|
||||
|
||||
{namespace === EIP155 ? (
|
||||
<>
|
||||
{isEIP1559 === false ? (
|
||||
<>
|
||||
<Text style={styles.dataBoxLabel}>
|
||||
{'Gas Price (wei)'}
|
||||
</Text>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={ethGasPrice?.toNumber().toString()}
|
||||
onChangeText={value =>
|
||||
setEthGasPrice(BigNumber.from(value))
|
||||
}
|
||||
style={styles.transactionFeesInput}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text style={styles.dataBoxLabel}>
|
||||
Max Fee Per Gas (wei)
|
||||
</Text>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={ethMaxFee?.toNumber().toString()}
|
||||
onChangeText={value => {
|
||||
if (IS_NUMBER_REGEX.test(value)) {
|
||||
setEthMaxFee(BigNumber.from(value));
|
||||
}
|
||||
}}
|
||||
style={styles.transactionFeesInput}
|
||||
/>
|
||||
<Text style={styles.dataBoxLabel}>
|
||||
Max Priority Fee Per Gas (wei)
|
||||
</Text>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={ethMaxPriorityFee?.toNumber().toString()}
|
||||
onChangeText={value => {
|
||||
if (IS_NUMBER_REGEX.test(value)) {
|
||||
setEthMaxPriorityFee(BigNumber.from(value));
|
||||
}
|
||||
}}
|
||||
style={styles.transactionFeesInput}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Text style={styles.dataBoxLabel}>Gas Limit</Text>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={ethGasLimit?.toNumber().toString()}
|
||||
onChangeText={value => {
|
||||
if (IS_NUMBER_REGEX.test(value)) {
|
||||
setEthGasLimit(BigNumber.from(value));
|
||||
}
|
||||
}}
|
||||
style={styles.transactionFeesInput}
|
||||
/>
|
||||
<DataBox
|
||||
label={`${
|
||||
isEIP1559 === true ? 'Max Fee' : 'Gas Fee'
|
||||
} (wei)`}
|
||||
data={fees!}
|
||||
/>
|
||||
<DataBox label="Data" data={transaction.data!} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text style={styles.dataBoxLabel}>{`Fee (${
|
||||
requestedNetwork!.nativeDenom
|
||||
})`}</Text>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={fees}
|
||||
onChangeText={value => setFees(value)}
|
||||
style={styles.transactionFeesInput}
|
||||
/>
|
||||
<Text style={styles.dataBoxLabel}>Gas Limit</Text>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={cosmosGasLimit}
|
||||
onChangeText={value => {
|
||||
if (IS_NUMBER_REGEX.test(value)) {
|
||||
setCosmosGasLimit(value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
<View style={styles.buttonContainer}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={acceptRequestHandler}
|
||||
loading={isTxLoading}
|
||||
disabled={!balance || !fees}>
|
||||
{isTxLoading ? 'Processing' : 'Yes'}
|
||||
</Button>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={rejectRequestHandler}
|
||||
buttonColor="#B82B0D">
|
||||
No
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
<TxErrorDialog
|
||||
error={txError!}
|
||||
visible={isTxErrorDialogOpen}
|
||||
hideDialog={() => {
|
||||
setIsTxErrorDialogOpen(false);
|
||||
if (!isSufficientFunds || !balance || !fees) {
|
||||
rejectRequestHandler();
|
||||
navigation.navigate('Laconic');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApproveTransfer;
|
197
src/screens/EditNetwork.tsx
Normal file
197
src/screens/EditNetwork.tsx
Normal file
@ -0,0 +1,197 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { useForm, Controller, FieldErrors } from 'react-hook-form';
|
||||
import { TextInput, Button, HelperText, Text } from 'react-native-paper';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import {
|
||||
NativeStackNavigationProp,
|
||||
NativeStackScreenProps,
|
||||
} from '@react-navigation/native-stack';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
import { setInternetCredentials } from '../utils/key-store';
|
||||
import { StackParamsList } from '../types';
|
||||
import styles from '../styles/stylesheet';
|
||||
import { retrieveNetworksData } from '../utils/accounts';
|
||||
import { useNetworks } from '../context/NetworksContext';
|
||||
import {
|
||||
COSMOS,
|
||||
EIP155,
|
||||
EMPTY_FIELD_ERROR,
|
||||
INVALID_URL_ERROR,
|
||||
} from '../utils/constants';
|
||||
|
||||
const ethNetworksFormSchema = z.object({
|
||||
// Adding type field for resolving typescript error
|
||||
type: z.literal(EIP155).optional(),
|
||||
networkName: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
|
||||
rpcUrl: z.string().url({ message: INVALID_URL_ERROR }),
|
||||
blockExplorerUrl: z
|
||||
.string()
|
||||
.url({ message: INVALID_URL_ERROR })
|
||||
.or(z.literal('')),
|
||||
});
|
||||
|
||||
const cosmosNetworksFormDataSchema = z.object({
|
||||
type: z.literal(COSMOS).optional(),
|
||||
networkName: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
|
||||
rpcUrl: z.string().url({ message: INVALID_URL_ERROR }),
|
||||
blockExplorerUrl: z
|
||||
.string()
|
||||
.url({ message: INVALID_URL_ERROR })
|
||||
.or(z.literal('')),
|
||||
gasPrice: z
|
||||
.string()
|
||||
.nonempty({ message: EMPTY_FIELD_ERROR })
|
||||
.regex(/^\d+(\.\d+)?$/),
|
||||
});
|
||||
|
||||
type EditNetworkProps = NativeStackScreenProps<StackParamsList, 'EditNetwork'>;
|
||||
|
||||
const EditNetwork = ({ route }: EditNetworkProps) => {
|
||||
const { setNetworksData } = useNetworks();
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<StackParamsList>>();
|
||||
|
||||
const networkData = route.params.selectedNetwork;
|
||||
|
||||
const networksFormDataSchema =
|
||||
networkData.namespace === COSMOS
|
||||
? cosmosNetworksFormDataSchema
|
||||
: ethNetworksFormSchema;
|
||||
|
||||
const {
|
||||
control,
|
||||
formState: { errors, isSubmitting },
|
||||
handleSubmit,
|
||||
} = useForm<z.infer<typeof networksFormDataSchema>>({
|
||||
mode: 'onChange',
|
||||
resolver: zodResolver(networksFormDataSchema),
|
||||
});
|
||||
|
||||
const submit = useCallback(
|
||||
async (data: z.infer<typeof networksFormDataSchema>) => {
|
||||
const retrievedNetworksData = await retrieveNetworksData();
|
||||
const { type, ...dataWithoutType } = data;
|
||||
const newNetworkData = { ...networkData, ...dataWithoutType };
|
||||
const index = retrievedNetworksData.findIndex(
|
||||
network => network.networkId === networkData.networkId,
|
||||
);
|
||||
|
||||
retrievedNetworksData.splice(index, 1, newNetworkData);
|
||||
|
||||
await setInternetCredentials(
|
||||
'networks',
|
||||
'_',
|
||||
JSON.stringify(retrievedNetworksData),
|
||||
);
|
||||
|
||||
setNetworksData(retrievedNetworksData);
|
||||
|
||||
navigation.navigate('Laconic');
|
||||
},
|
||||
[networkData, navigation, setNetworksData],
|
||||
);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View>
|
||||
<Text style={styles.subHeading}>
|
||||
Edit {networkData?.networkName} details
|
||||
</Text>
|
||||
</View>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue={networkData.networkName}
|
||||
name="networkName"
|
||||
render={({ field: { onChange, onBlur, value } }) => (
|
||||
<>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
label="Network Name"
|
||||
value={value}
|
||||
onBlur={onBlur}
|
||||
onChangeText={textValue => onChange(textValue)}
|
||||
/>
|
||||
<HelperText type="error">{errors.networkName?.message}</HelperText>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="rpcUrl"
|
||||
defaultValue={networkData.rpcUrl}
|
||||
render={({ field: { onChange, onBlur, value } }) => (
|
||||
<>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
label="New RPC URL"
|
||||
onBlur={onBlur}
|
||||
value={value}
|
||||
onChangeText={textValue => onChange(textValue)}
|
||||
/>
|
||||
<HelperText type="error">{errors.rpcUrl?.message}</HelperText>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue={networkData.blockExplorerUrl}
|
||||
name="blockExplorerUrl"
|
||||
render={({ field: { onChange, onBlur, value } }) => (
|
||||
<>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={value}
|
||||
label="Block Explorer URL (Optional)"
|
||||
onBlur={onBlur}
|
||||
onChangeText={textValue => onChange(textValue)}
|
||||
/>
|
||||
<HelperText type="error">
|
||||
{errors.blockExplorerUrl?.message}
|
||||
</HelperText>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
{networkData.namespace === COSMOS && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="gasPrice"
|
||||
defaultValue={networkData.gasPrice}
|
||||
render={({ field: { onChange, onBlur, value } }) => (
|
||||
<>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={value}
|
||||
label="Gas Price"
|
||||
onBlur={onBlur}
|
||||
onChangeText={onChange}
|
||||
/>
|
||||
<HelperText type="error">
|
||||
{
|
||||
(
|
||||
errors as FieldErrors<
|
||||
z.infer<typeof cosmosNetworksFormDataSchema>
|
||||
>
|
||||
).gasPrice?.message
|
||||
}
|
||||
</HelperText>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
mode="contained"
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
onPress={handleSubmit(submit)}>
|
||||
{isSubmitting ? 'Adding' : 'Submit'}
|
||||
</Button>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditNetwork;
|
176
src/screens/HomeScreen.tsx
Normal file
176
src/screens/HomeScreen.tsx
Normal file
@ -0,0 +1,176 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { View, ActivityIndicator, Image } from 'react-native';
|
||||
import { Button, Text } from 'react-native-paper';
|
||||
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
// import { getSdkError } from '@walletconnect/utils';
|
||||
|
||||
import { createWallet, resetWallet, retrieveAccounts } from '../utils/accounts';
|
||||
import { DialogComponent } from '../components/Dialog';
|
||||
import { NetworkDropdown } from '../components/NetworkDropdown';
|
||||
import Accounts from '../components/Accounts';
|
||||
import CreateWallet from '../components/CreateWallet';
|
||||
import ConfirmDialog from '../components/ConfirmDialog';
|
||||
import styles from '../styles/stylesheet';
|
||||
import { useAccounts } from '../context/AccountsContext';
|
||||
import { useWalletConnect } from '../context/WalletConnectContext';
|
||||
import { NetworksDataState, StackParamsList } from '../types';
|
||||
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
|
||||
import { useNetworks } from '../context/NetworksContext';
|
||||
|
||||
const WCLogo = () => {
|
||||
return (
|
||||
<Image
|
||||
style={styles.walletConnectLogo}
|
||||
source={require('../assets/WalletConnect-Icon-Blueberry.png')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const HomeScreen = () => {
|
||||
const { accounts, setAccounts, setCurrentIndex } = useAccounts();
|
||||
|
||||
const { networksData, selectedNetwork, setSelectedNetwork, setNetworksData } =
|
||||
useNetworks();
|
||||
const { setActiveSessions } = useWalletConnect();
|
||||
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<StackParamsList>>();
|
||||
useEffect(() => {
|
||||
if (accounts.length > 0) {
|
||||
navigation.setOptions({
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
headerRight: () => (
|
||||
<Button onPress={() => navigation.navigate('WalletConnect')}>
|
||||
{<WCLogo />}
|
||||
</Button>
|
||||
),
|
||||
});
|
||||
} else {
|
||||
navigation.setOptions({
|
||||
headerRight: undefined,
|
||||
});
|
||||
}
|
||||
}, [navigation, accounts]);
|
||||
|
||||
const [isWalletCreated, setIsWalletCreated] = useState<boolean>(false);
|
||||
const [isWalletCreating, setIsWalletCreating] = useState<boolean>(false);
|
||||
const [walletDialog, setWalletDialog] = useState<boolean>(false);
|
||||
const [resetWalletDialog, setResetWalletDialog] = useState<boolean>(false);
|
||||
// const [isAccountsFetched, setIsAccountsFetched] = useState<boolean>(false);
|
||||
const [isAccountsFetched, setIsAccountsFetched] = useState<boolean>(true);
|
||||
const [phrase, setPhrase] = useState('');
|
||||
|
||||
const hideWalletDialog = () => setWalletDialog(false);
|
||||
const hideResetDialog = () => setResetWalletDialog(false);
|
||||
|
||||
const fetchAccounts = useCallback(async () => {
|
||||
if (!selectedNetwork) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loadedAccounts = await retrieveAccounts(selectedNetwork);
|
||||
|
||||
if (loadedAccounts) {
|
||||
setAccounts(loadedAccounts);
|
||||
setIsWalletCreated(true);
|
||||
}
|
||||
|
||||
setIsAccountsFetched(true);
|
||||
}, [selectedNetwork, setAccounts]);
|
||||
|
||||
const createWalletHandler = async () => {
|
||||
setIsWalletCreating(true);
|
||||
const mnemonic = await createWallet(networksData);
|
||||
if (mnemonic) {
|
||||
fetchAccounts();
|
||||
setWalletDialog(true);
|
||||
setPhrase(mnemonic);
|
||||
setSelectedNetwork(networksData[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmResetWallet = useCallback(async () => {
|
||||
setIsWalletCreated(false);
|
||||
setIsWalletCreating(false);
|
||||
setAccounts([]);
|
||||
setCurrentIndex(0);
|
||||
setNetworksData([]);
|
||||
setSelectedNetwork(undefined);
|
||||
await resetWallet();
|
||||
// const sessions = web3wallet!.getActiveSessions();
|
||||
|
||||
// Object.keys(sessions).forEach(async sessionId => {
|
||||
// await web3wallet!.disconnectSession({
|
||||
// topic: sessionId,
|
||||
// reason: getSdkError('USER_DISCONNECTED'),
|
||||
// });
|
||||
// });
|
||||
setActiveSessions({});
|
||||
|
||||
hideResetDialog();
|
||||
}, [
|
||||
setAccounts,
|
||||
setActiveSessions,
|
||||
setCurrentIndex,
|
||||
setNetworksData,
|
||||
setSelectedNetwork,
|
||||
]);
|
||||
|
||||
const updateNetwork = (networkData: NetworksDataState) => {
|
||||
setSelectedNetwork(networkData);
|
||||
setCurrentIndex(0);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchAccounts();
|
||||
}, [networksData, setAccounts, selectedNetwork, fetchAccounts]);
|
||||
|
||||
return (
|
||||
<View style={styles.appContainer}>
|
||||
{!isAccountsFetched ? (
|
||||
<View style={styles.spinnerContainer}>
|
||||
<Text style={styles.LoadingText}>Loading...</Text>
|
||||
<ActivityIndicator size="large" color="#0000ff" />
|
||||
</View>
|
||||
) : isWalletCreated && selectedNetwork ? (
|
||||
<>
|
||||
<NetworkDropdown updateNetwork={updateNetwork} />
|
||||
<View style={styles.accountComponent}>
|
||||
<Accounts />
|
||||
</View>
|
||||
<View style={styles.resetContainer}>
|
||||
<Button
|
||||
style={styles.resetButton}
|
||||
mode="contained"
|
||||
buttonColor="#B82B0D"
|
||||
onPress={() => {
|
||||
setResetWalletDialog(true);
|
||||
}}>
|
||||
Reset Wallet
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<CreateWallet
|
||||
isWalletCreating={isWalletCreating}
|
||||
createWalletHandler={createWalletHandler}
|
||||
/>
|
||||
)}
|
||||
<DialogComponent
|
||||
visible={walletDialog}
|
||||
hideDialog={hideWalletDialog}
|
||||
contentText={phrase}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
title="Reset wallet"
|
||||
visible={resetWalletDialog}
|
||||
hideDialog={hideResetDialog}
|
||||
onConfirm={confirmResetWallet}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomeScreen;
|
30
src/screens/InvalidPath.tsx
Normal file
30
src/screens/InvalidPath.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
import { StackParamsList } from '../types';
|
||||
import styles from '../styles/stylesheet';
|
||||
|
||||
const InvalidPath = () => {
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<StackParamsList>>();
|
||||
return (
|
||||
<View style={styles.badRequestContainer}>
|
||||
<Text style={styles.invalidMessageText}>
|
||||
The signature request was invalid.
|
||||
</Text>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => {
|
||||
navigation.navigate('Laconic');
|
||||
}}>
|
||||
Home
|
||||
</Button>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default InvalidPath;
|
60
src/screens/SignMessage.tsx
Normal file
60
src/screens/SignMessage.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Alert } from 'react-native';
|
||||
import { Button, Text, TextInput } from 'react-native-paper';
|
||||
|
||||
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
|
||||
import { StackParamsList } from '../types';
|
||||
import styles from '../styles/stylesheet';
|
||||
import { signMessage } from '../utils/sign-message';
|
||||
import AccountDetails from '../components/AccountDetails';
|
||||
|
||||
type SignProps = NativeStackScreenProps<StackParamsList, 'SignMessage'>;
|
||||
|
||||
const SignMessage = ({ route }: SignProps) => {
|
||||
const namespace = route.params.selectedNamespace;
|
||||
const chainId = route.params.selectedChainId;
|
||||
const account = route.params.accountInfo;
|
||||
|
||||
const [message, setMessage] = useState<string>('');
|
||||
|
||||
const signMessageHandler = async () => {
|
||||
const signedMessage = await signMessage({
|
||||
message,
|
||||
namespace,
|
||||
chainId,
|
||||
accountId: account.index,
|
||||
});
|
||||
Alert.alert('Signature', signedMessage);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.signPage}>
|
||||
<View style={styles.accountInfo}>
|
||||
<View>
|
||||
<Text variant="titleMedium">
|
||||
{account && `Account ${account.index + 1}`}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.accountContainer}>
|
||||
<AccountDetails account={account} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
placeholder="Enter your message"
|
||||
onChangeText={text => setMessage(text)}
|
||||
value={message}
|
||||
/>
|
||||
|
||||
<View style={styles.signButton}>
|
||||
<Button mode="contained" onPress={signMessageHandler}>
|
||||
Sign
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignMessage;
|
330
src/screens/SignRequest.tsx
Normal file
330
src/screens/SignRequest.tsx
Normal file
@ -0,0 +1,330 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Alert, Image, ScrollView, View } from 'react-native';
|
||||
import { ActivityIndicator, Button, Text, Appbar } from 'react-native-paper';
|
||||
import { SvgUri } from 'react-native-svg';
|
||||
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import {
|
||||
NativeStackNavigationProp,
|
||||
NativeStackScreenProps,
|
||||
} from '@react-navigation/native-stack';
|
||||
import { getHeaderTitle } from '@react-navigation/elements';
|
||||
|
||||
import { Account, StackParamsList } from '../types';
|
||||
import AccountDetails from '../components/AccountDetails';
|
||||
import styles from '../styles/stylesheet';
|
||||
import { signMessage } from '../utils/sign-message';
|
||||
import { retrieveSingleAccount } from '../utils/accounts';
|
||||
import {
|
||||
approveWalletConnectRequest,
|
||||
rejectWalletConnectRequest,
|
||||
WalletConnectRequests,
|
||||
} from '../utils/wallet-connect/wallet-connect-requests';
|
||||
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
|
||||
import { EIP155_SIGNING_METHODS } from '../utils/wallet-connect/EIP155Data';
|
||||
import { useNetworks } from '../context/NetworksContext';
|
||||
import { COSMOS_METHODS } from '../utils/wallet-connect/COSMOSData';
|
||||
|
||||
type SignRequestProps = NativeStackScreenProps<StackParamsList, 'SignRequest'>;
|
||||
|
||||
const SignRequest = ({ route }: SignRequestProps) => {
|
||||
const { networksData } = useNetworks();
|
||||
|
||||
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 [account, setAccount] = useState<Account>();
|
||||
const [message, setMessage] = useState<string>('');
|
||||
const [namespace, setNamespace] = useState<string>('');
|
||||
const [chainId, setChainId] = useState<string>('');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isApproving, setIsApproving] = useState(false);
|
||||
const [isRejecting, setIsRejecting] = useState(false);
|
||||
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<StackParamsList>>();
|
||||
|
||||
const isCosmosSignDirect = useMemo(() => {
|
||||
const requestParams = route.params.requestEvent;
|
||||
|
||||
if (!requestParams) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return requestParams.params.request.method === 'cosmos_signDirect';
|
||||
}, [route.params]);
|
||||
|
||||
const isEthSendTransaction = useMemo(() => {
|
||||
const requestParams = route.params.requestEvent;
|
||||
|
||||
if (!requestParams) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
requestParams.params.request.method ===
|
||||
EIP155_SIGNING_METHODS.ETH_SEND_TRANSACTION
|
||||
);
|
||||
}, [route.params]);
|
||||
|
||||
const retrieveData = useCallback(
|
||||
async (
|
||||
requestNamespace: string,
|
||||
requestChainId: string,
|
||||
requestAddress: string,
|
||||
requestMessage: string,
|
||||
) => {
|
||||
const requestAccount = await retrieveSingleAccount(
|
||||
requestNamespace,
|
||||
requestChainId,
|
||||
requestAddress,
|
||||
);
|
||||
if (!requestAccount) {
|
||||
navigation.navigate('InvalidPath');
|
||||
return;
|
||||
}
|
||||
|
||||
if (requestAccount !== account) {
|
||||
setAccount(requestAccount);
|
||||
}
|
||||
if (requestMessage !== message) {
|
||||
setMessage(decodeURIComponent(requestMessage));
|
||||
}
|
||||
if (requestNamespace !== namespace) {
|
||||
setNamespace(requestNamespace);
|
||||
}
|
||||
if (requestChainId !== chainId) {
|
||||
setChainId(requestChainId);
|
||||
}
|
||||
setIsLoading(false);
|
||||
},
|
||||
[account, message, navigation, namespace, chainId],
|
||||
);
|
||||
|
||||
const sanitizePath = useCallback(
|
||||
(path: string) => {
|
||||
const regex = /^\/sign\/(eip155|cosmos)\/(.+)\/(.+)\/(.+)$/;
|
||||
const match = path.match(regex);
|
||||
if (match) {
|
||||
const [, pathNamespace, pathChainId, pathAddress, pathMessage] = match;
|
||||
return {
|
||||
namespace: pathNamespace,
|
||||
chainId: pathChainId,
|
||||
address: pathAddress,
|
||||
message: pathMessage,
|
||||
};
|
||||
} else {
|
||||
navigation.navigate('InvalidPath');
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[navigation],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (route.path) {
|
||||
const sanitizedRoute = sanitizePath(route.path);
|
||||
sanitizedRoute &&
|
||||
retrieveData(
|
||||
sanitizedRoute.namespace,
|
||||
sanitizedRoute.chainId,
|
||||
sanitizedRoute.address,
|
||||
sanitizedRoute.message,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const requestEvent = route.params.requestEvent;
|
||||
const requestChainId = requestEvent?.params.chainId;
|
||||
|
||||
const requestedChain = networksData.find(
|
||||
networkData => networkData.chainId === requestChainId?.split(':')[1],
|
||||
);
|
||||
|
||||
retrieveData(
|
||||
requestedChain!.namespace,
|
||||
requestedChain!.chainId,
|
||||
route.params.address,
|
||||
route.params.message,
|
||||
);
|
||||
}, [retrieveData, sanitizePath, route, networksData]);
|
||||
|
||||
const handleWalletConnectRequest = async () => {
|
||||
const { requestEvent } = route.params || {};
|
||||
|
||||
if (!account) {
|
||||
throw new Error('account not found');
|
||||
}
|
||||
|
||||
if (!requestEvent) {
|
||||
throw new Error('Request event not found');
|
||||
}
|
||||
|
||||
let options: WalletConnectRequests;
|
||||
|
||||
switch (requestEvent.params.request.method) {
|
||||
case COSMOS_METHODS.COSMOS_SIGN_DIRECT:
|
||||
options = {
|
||||
type: 'cosmos_signDirect',
|
||||
message,
|
||||
};
|
||||
break;
|
||||
case COSMOS_METHODS.COSMOS_SIGN_AMINO:
|
||||
options = {
|
||||
type: 'cosmos_signAmino',
|
||||
message,
|
||||
};
|
||||
break;
|
||||
case EIP155_SIGNING_METHODS.PERSONAL_SIGN:
|
||||
options = {
|
||||
type: 'personal_sign',
|
||||
message,
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error('Invalid Method');
|
||||
}
|
||||
|
||||
const response = await approveWalletConnectRequest(
|
||||
requestEvent,
|
||||
account,
|
||||
namespace,
|
||||
chainId,
|
||||
options,
|
||||
);
|
||||
|
||||
const { topic } = requestEvent;
|
||||
await web3wallet!.respondSessionRequest({ topic, response });
|
||||
};
|
||||
|
||||
const handleIntent = async () => {
|
||||
if (!account) {
|
||||
throw new Error('Account is not valid');
|
||||
}
|
||||
if (message) {
|
||||
const signedMessage = await signMessage({
|
||||
message,
|
||||
namespace,
|
||||
chainId,
|
||||
accountId: account.index,
|
||||
});
|
||||
Alert.alert('Signature', signedMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const signMessageHandler = async () => {
|
||||
setIsApproving(true);
|
||||
if (route.params.requestEvent) {
|
||||
await handleWalletConnectRequest();
|
||||
} else {
|
||||
await handleIntent();
|
||||
}
|
||||
|
||||
setIsApproving(false);
|
||||
navigation.navigate('Laconic');
|
||||
};
|
||||
|
||||
const rejectRequestHandler = async () => {
|
||||
setIsRejecting(true);
|
||||
if (route.params?.requestEvent) {
|
||||
const response = rejectWalletConnectRequest(route.params?.requestEvent);
|
||||
const { topic } = route.params?.requestEvent;
|
||||
await web3wallet!.respondSessionRequest({
|
||||
topic,
|
||||
response,
|
||||
});
|
||||
}
|
||||
|
||||
setIsRejecting(false);
|
||||
navigation.navigate('Laconic');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
header: ({ options, back }) => {
|
||||
const title = getHeaderTitle(options, 'Sign Request');
|
||||
|
||||
return (
|
||||
<Appbar.Header>
|
||||
{back && (
|
||||
<Appbar.BackAction
|
||||
onPress={async () => {
|
||||
await rejectRequestHandler();
|
||||
navigation.navigate('Laconic');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Appbar.Content title={title} />
|
||||
</Appbar.Header>
|
||||
);
|
||||
},
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [navigation, route.name]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<View style={styles.spinnerContainer}>
|
||||
<ActivityIndicator size="large" color="#0000ff" />
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<ScrollView contentContainerStyle={styles.appContainer}>
|
||||
<View style={styles.dappDetails}>
|
||||
{requestIcon && (
|
||||
<>
|
||||
{requestIcon.endsWith('.svg') ? (
|
||||
<View style={styles.dappLogo}>
|
||||
<SvgUri height="50" width="50" uri={requestIcon} />
|
||||
</View>
|
||||
) : (
|
||||
<Image
|
||||
style={styles.dappLogo}
|
||||
source={{ uri: requestIcon }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Text>{requestName}</Text>
|
||||
<Text variant="bodyMedium">{requestURL}</Text>
|
||||
</View>
|
||||
<AccountDetails account={account} />
|
||||
{isCosmosSignDirect || isEthSendTransaction ? (
|
||||
<View style={styles.requestDirectMessage}>
|
||||
<ScrollView nestedScrollEnabled>
|
||||
<Text variant="bodyLarge">{message}</Text>
|
||||
</ScrollView>
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.requestMessage}>
|
||||
<Text variant="bodyLarge">{message}</Text>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
<View style={styles.buttonContainer}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={signMessageHandler}
|
||||
loading={isApproving}
|
||||
disabled={isApproving}>
|
||||
Yes
|
||||
</Button>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={rejectRequestHandler}
|
||||
loading={isRejecting}
|
||||
buttonColor="#B82B0D">
|
||||
No
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignRequest;
|
84
src/screens/WalletConnect.tsx
Normal file
84
src/screens/WalletConnect.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Image, TouchableOpacity, View } from 'react-native';
|
||||
import { List, Text } from 'react-native-paper';
|
||||
import { SvgUri } from 'react-native-svg';
|
||||
|
||||
// import { getSdkError } from '@walletconnect/utils';
|
||||
|
||||
import { useWalletConnect } from '../context/WalletConnectContext';
|
||||
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
|
||||
import styles from '../styles/stylesheet';
|
||||
|
||||
export default function WalletConnect() {
|
||||
const { activeSessions, setActiveSessions } = useWalletConnect();
|
||||
|
||||
const disconnect = async (sessionId: string) => {
|
||||
// await web3wallet!.disconnectSession({
|
||||
// topic: sessionId,
|
||||
// reason: getSdkError('USER_DISCONNECTED'),
|
||||
// });
|
||||
const sessions = web3wallet?.getActiveSessions() || {};
|
||||
setActiveSessions(sessions);
|
||||
return;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const sessions = web3wallet!.getActiveSessions();
|
||||
setActiveSessions(sessions);
|
||||
}, [setActiveSessions]);
|
||||
|
||||
return (
|
||||
<View>
|
||||
{Object.keys(activeSessions).length > 0 ? (
|
||||
<>
|
||||
<View style={styles.sessionsContainer}>
|
||||
<Text variant="titleMedium">Active Sessions</Text>
|
||||
</View>
|
||||
<List.Section>
|
||||
{Object.entries(activeSessions).map(([sessionId, session]) => (
|
||||
<List.Item
|
||||
style={styles.sessionItem}
|
||||
key={sessionId}
|
||||
title={`${session.peer.metadata.name}`}
|
||||
descriptionNumberOfLines={7}
|
||||
description={`${sessionId} \n\n${session.peer.metadata.url}\n\n${session.peer.metadata.description}`}
|
||||
// reference: https://github.com/react-navigation/react-navigation/issues/11371#issuecomment-1546543183
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
left={() => (
|
||||
<>
|
||||
{session.peer.metadata.icons[0].endsWith('.svg') ? (
|
||||
<View style={styles.dappLogo}>
|
||||
<SvgUri
|
||||
height="50"
|
||||
width="50"
|
||||
uri={session.peer.metadata.icons[0]}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<Image
|
||||
style={styles.dappLogo}
|
||||
source={{ uri: session.peer.metadata.icons[0] }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
right={() => (
|
||||
<TouchableOpacity
|
||||
onPress={() => disconnect(sessionId)}
|
||||
style={styles.disconnectSession}>
|
||||
<List.Icon icon="close" />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</List.Section>
|
||||
</>
|
||||
) : (
|
||||
<View style={styles.noActiveSessions}>
|
||||
<Text>You have no active sessions</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
5
src/setupTests.ts
Normal file
5
src/setupTests.ts
Normal file
@ -0,0 +1,5 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
287
src/styles/stylesheet.js
Normal file
287
src/styles/stylesheet.js
Normal file
@ -0,0 +1,287 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
createWalletContainer: {
|
||||
marginTop: 20,
|
||||
width: 150,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
signLink: {
|
||||
alignItems: 'flex-end',
|
||||
marginTop: 24,
|
||||
},
|
||||
hyperlink: {
|
||||
fontWeight: '500',
|
||||
textDecorationLine: 'underline',
|
||||
},
|
||||
highlight: {
|
||||
fontWeight: '700',
|
||||
},
|
||||
accountContainer: {
|
||||
padding: 8,
|
||||
paddingBottom: 0,
|
||||
},
|
||||
addAccountButton: {
|
||||
marginTop: 24,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
accountComponent: {
|
||||
flex: 4,
|
||||
},
|
||||
appContainer: {
|
||||
flexGrow: 1,
|
||||
marginTop: 24,
|
||||
paddingHorizontal: 24,
|
||||
},
|
||||
resetContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
resetButton: {
|
||||
alignSelf: 'center',
|
||||
},
|
||||
signButton: {
|
||||
marginTop: 20,
|
||||
width: 150,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
signPage: {
|
||||
paddingHorizontal: 24,
|
||||
},
|
||||
addNetwork: {
|
||||
paddingHorizontal: 24,
|
||||
marginTop: 30,
|
||||
},
|
||||
accountInfo: {
|
||||
marginTop: 12,
|
||||
marginBottom: 30,
|
||||
},
|
||||
networkDropdown: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
dialogTitle: {
|
||||
padding: 10,
|
||||
},
|
||||
dialogContents: {
|
||||
marginTop: 24,
|
||||
padding: 10,
|
||||
borderWidth: 1,
|
||||
borderRadius: 10,
|
||||
},
|
||||
dialogWarning: {
|
||||
color: 'red',
|
||||
},
|
||||
gridContainer: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
gridItem: {
|
||||
width: '25%',
|
||||
margin: 8,
|
||||
padding: 6,
|
||||
borderWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
HDcontainer: {
|
||||
marginTop: 24,
|
||||
paddingHorizontal: 8,
|
||||
},
|
||||
HDrowContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
HDtext: {
|
||||
color: 'black',
|
||||
fontSize: 18,
|
||||
margin: 4,
|
||||
},
|
||||
HDtextInput: {
|
||||
flex: 1,
|
||||
},
|
||||
HDbuttonContainer: {
|
||||
marginTop: 20,
|
||||
width: 200,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
spinnerContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
LoadingText: {
|
||||
color: 'black',
|
||||
fontSize: 18,
|
||||
padding: 10,
|
||||
},
|
||||
requestMessage: {
|
||||
borderWidth: 1,
|
||||
borderRadius: 5,
|
||||
marginTop: 50,
|
||||
height: 'auto',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 10,
|
||||
},
|
||||
requestDirectMessage: {
|
||||
borderWidth: 1,
|
||||
borderRadius: 5,
|
||||
marginTop: 20,
|
||||
marginBottom: 50,
|
||||
height: 500,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 8,
|
||||
},
|
||||
approveTransfer: {
|
||||
height: '40%',
|
||||
marginBottom: 30,
|
||||
},
|
||||
buttonContainer: {
|
||||
flexDirection: 'row',
|
||||
marginLeft: 20,
|
||||
marginBottom: 10,
|
||||
justifyContent: 'space-evenly',
|
||||
},
|
||||
badRequestContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
invalidMessageText: {
|
||||
color: 'black',
|
||||
fontSize: 16,
|
||||
textAlign: 'center',
|
||||
marginBottom: 20,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 10,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
modalContentContainer: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: 34,
|
||||
borderBottomStartRadius: 0,
|
||||
borderBottomEndRadius: 0,
|
||||
borderWidth: 1,
|
||||
width: '100%',
|
||||
height: '50%',
|
||||
position: 'absolute',
|
||||
backgroundColor: 'white',
|
||||
bottom: 0,
|
||||
},
|
||||
modalOuterContainer: { flex: 1 },
|
||||
dappLogo: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: 8,
|
||||
marginVertical: 16,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
space: {
|
||||
width: 50,
|
||||
},
|
||||
flexRow: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginTop: 20,
|
||||
paddingHorizontal: 16,
|
||||
marginBottom: 10,
|
||||
},
|
||||
marginVertical8: {
|
||||
marginVertical: 8,
|
||||
textAlign: 'center',
|
||||
},
|
||||
subHeading: {
|
||||
textAlign: 'center',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 10,
|
||||
marginTop: 10,
|
||||
},
|
||||
centerText: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
messageBody: {
|
||||
borderWidth: 1,
|
||||
borderRadius: 6,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 10,
|
||||
marginVertical: 3,
|
||||
},
|
||||
cameraContainer: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
inputContainer: {
|
||||
marginTop: 20,
|
||||
},
|
||||
camera: {
|
||||
width: 400,
|
||||
height: 400,
|
||||
},
|
||||
dappDetails: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
dataBoxContainer: {
|
||||
marginBottom: 10,
|
||||
},
|
||||
dataBoxLabel: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 3,
|
||||
color: 'black',
|
||||
},
|
||||
dataBox: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
padding: 10,
|
||||
borderRadius: 5,
|
||||
},
|
||||
dataBoxData: {
|
||||
fontSize: 16,
|
||||
color: 'black',
|
||||
},
|
||||
transactionText: {
|
||||
padding: 8,
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
color: 'black',
|
||||
},
|
||||
balancePadding: {
|
||||
padding: 8,
|
||||
},
|
||||
noActiveSessions: { display: 'flex', alignItems: 'center', marginTop: 12 },
|
||||
disconnectSession: { display: 'flex', justifyContent: 'center' },
|
||||
sessionItem: { paddingLeft: 12, borderBottomWidth: 0.5 },
|
||||
sessionsContainer: { paddingLeft: 12, borderBottomWidth: 0.5 },
|
||||
walletConnectUriText: { padding: 10 },
|
||||
walletConnectLogo: { width: 24, height: 15, margin: 0 },
|
||||
selectNetworkText: {
|
||||
fontWeight: 'bold',
|
||||
marginVertical: 10,
|
||||
},
|
||||
transactionFeesInput: { marginBottom: 10 },
|
||||
approveTransaction: {
|
||||
flexGrow: 1,
|
||||
marginTop: 0,
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 5,
|
||||
},
|
||||
transactionLabel: {
|
||||
fontWeight: '700',
|
||||
padding: 8,
|
||||
},
|
||||
});
|
||||
|
||||
export default styles;
|
140
src/types.ts
Normal file
140
src/types.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import { PopulatedTransaction } from 'ethers';
|
||||
|
||||
// import { SignClientTypes, SessionTypes } from '@walletconnect/types';
|
||||
// import { Web3WalletTypes } from '@walletconnect/web3wallet';
|
||||
import { EncodeObject } from '@cosmjs/proto-signing';
|
||||
|
||||
export type StackParamsList = {
|
||||
Laconic: undefined;
|
||||
SignMessage: {
|
||||
selectedNamespace: string;
|
||||
selectedChainId: string;
|
||||
accountInfo: Account;
|
||||
};
|
||||
SignRequest: {
|
||||
namespace: string;
|
||||
address: string;
|
||||
message: string;
|
||||
// requestEvent?: Web3WalletTypes.SessionRequest;
|
||||
// requestSessionData?: SessionTypes.Struct;
|
||||
requestEvent?: any;
|
||||
requestSessionData?: any;
|
||||
};
|
||||
ApproveTransfer: {
|
||||
transaction: PopulatedTransaction;
|
||||
// requestEvent: Web3WalletTypes.SessionRequest;
|
||||
// requestSessionData: SessionTypes.Struct;
|
||||
requestEvent: any;
|
||||
requestSessionData: any;
|
||||
};
|
||||
InvalidPath: undefined;
|
||||
WalletConnect: undefined;
|
||||
AddSession: undefined;
|
||||
AddNetwork: undefined;
|
||||
EditNetwork: {
|
||||
selectedNetwork: NetworksDataState;
|
||||
};
|
||||
ApproveTransaction: {
|
||||
transactionMessage: EncodeObject;
|
||||
signer: string;
|
||||
// requestEvent: Web3WalletTypes.SessionRequest;
|
||||
// requestSessionData: SessionTypes.Struct;
|
||||
requestEvent: any;
|
||||
requestSessionData: any;
|
||||
};
|
||||
};
|
||||
|
||||
export type Account = {
|
||||
index: number;
|
||||
pubKey: string;
|
||||
address: string;
|
||||
hdPath: string;
|
||||
};
|
||||
|
||||
export type NetworkDropdownProps = {
|
||||
updateNetwork: (networksData: NetworksDataState) => void;
|
||||
};
|
||||
|
||||
export type NetworksFormData = {
|
||||
networkName: string;
|
||||
rpcUrl: string;
|
||||
chainId: string;
|
||||
currencySymbol?: string;
|
||||
blockExplorerUrl?: string;
|
||||
namespace: string;
|
||||
nativeDenom?: string;
|
||||
addressPrefix?: string;
|
||||
coinType?: string;
|
||||
gasPrice?: string;
|
||||
isDefault: boolean;
|
||||
};
|
||||
|
||||
export interface NetworksDataState extends NetworksFormData {
|
||||
networkId: string;
|
||||
}
|
||||
|
||||
export type SignMessageParams = {
|
||||
message: string;
|
||||
namespace: string;
|
||||
chainId: string;
|
||||
accountId: number;
|
||||
};
|
||||
|
||||
export type CreateWalletProps = {
|
||||
isWalletCreating: boolean;
|
||||
createWalletHandler: () => Promise<void>;
|
||||
};
|
||||
|
||||
export type ResetDialogProps = {
|
||||
title: string;
|
||||
visible: boolean;
|
||||
hideDialog: () => void;
|
||||
onConfirm: () => void;
|
||||
};
|
||||
|
||||
export type HDPathDialogProps = {
|
||||
pathCode: string;
|
||||
visible: boolean;
|
||||
hideDialog: () => void;
|
||||
updateAccounts: (account: Account) => void;
|
||||
};
|
||||
|
||||
export type CustomDialogProps = {
|
||||
visible: boolean;
|
||||
hideDialog: () => void;
|
||||
contentText: string;
|
||||
titleText?: string;
|
||||
};
|
||||
|
||||
export type GridViewProps = {
|
||||
words: string[];
|
||||
};
|
||||
|
||||
export type PathState = {
|
||||
firstNumber: string;
|
||||
secondNumber: string;
|
||||
thirdNumber: string;
|
||||
};
|
||||
|
||||
export interface PairingModalProps {
|
||||
visible: boolean;
|
||||
setModalVisible: (arg1: boolean) => void;
|
||||
currentProposal:
|
||||
// | SignClientTypes.EventArguments['session_proposal']
|
||||
| undefined;
|
||||
setCurrentProposal: (
|
||||
arg1:
|
||||
// | SignClientTypes.EventArguments['session_proposal']
|
||||
| undefined,
|
||||
) => void;
|
||||
setToastVisible: (arg1: boolean) => void;
|
||||
}
|
||||
|
||||
export interface WalletConnectContextProps {
|
||||
// activeSessions: Record<string, SessionTypes.Struct>;
|
||||
activeSessions: Record<string, any>;
|
||||
setActiveSessions: (
|
||||
// activeSessions: Record<string, SessionTypes.Struct>,
|
||||
activeSessions: Record<string, any>,
|
||||
) => void;
|
||||
}
|
346
src/utils/accounts.ts
Normal file
346
src/utils/accounts.ts
Normal file
@ -0,0 +1,346 @@
|
||||
/* Importing this library provides react native with a secure random source.
|
||||
For more information, "visit https://docs.ethers.org/v5/cookbook/react-native/#cookbook-reactnative-security" */
|
||||
import 'react-native-get-random-values';
|
||||
|
||||
import '@ethersproject/shims';
|
||||
|
||||
import { utils } from 'ethers';
|
||||
import { HDNode } from 'ethers/lib/utils';
|
||||
|
||||
import {
|
||||
setInternetCredentials,
|
||||
resetInternetCredentials,
|
||||
getInternetCredentials,
|
||||
} from './key-store';
|
||||
import { Secp256k1HdWallet } from '@cosmjs/amino';
|
||||
import { AccountData } from '@cosmjs/proto-signing';
|
||||
import { stringToPath } from '@cosmjs/crypto';
|
||||
|
||||
import { Account, NetworksDataState, NetworksFormData } from '../types';
|
||||
import {
|
||||
getHDPath,
|
||||
getPathKey,
|
||||
resetKeyServers,
|
||||
updateAccountIndices,
|
||||
} from './misc';
|
||||
import { COSMOS, EIP155 } from './constants';
|
||||
|
||||
const createWallet = async (
|
||||
networksData: NetworksDataState[],
|
||||
): Promise<string> => {
|
||||
const mnemonic = utils.entropyToMnemonic(utils.randomBytes(16));
|
||||
await setInternetCredentials('mnemonicServer', 'mnemonic', mnemonic);
|
||||
|
||||
const hdNode = HDNode.fromMnemonic(mnemonic);
|
||||
|
||||
for (const network of networksData) {
|
||||
const hdPath = `m/44'/${network.coinType}'/0'/0/0`;
|
||||
const node = hdNode.derivePath(hdPath);
|
||||
let address;
|
||||
|
||||
switch (network.namespace) {
|
||||
case EIP155:
|
||||
address = node.address;
|
||||
break;
|
||||
|
||||
case COSMOS:
|
||||
address = (
|
||||
await getCosmosAccounts(mnemonic, hdPath, network.addressPrefix)
|
||||
).data.address;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error('Unsupported namespace');
|
||||
}
|
||||
|
||||
const accountInfo = `${hdPath},${node.privateKey},${node.publicKey},${address}`;
|
||||
|
||||
await Promise.all([
|
||||
setInternetCredentials(
|
||||
`accounts/${network.namespace}:${network.chainId}/0`,
|
||||
'_',
|
||||
accountInfo,
|
||||
),
|
||||
setInternetCredentials(
|
||||
`addAccountCounter/${network.namespace}:${network.chainId}`,
|
||||
'_',
|
||||
'1',
|
||||
),
|
||||
setInternetCredentials(
|
||||
`accountIndices/${network.namespace}:${network.chainId}`,
|
||||
'_',
|
||||
'0',
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
return mnemonic;
|
||||
};
|
||||
|
||||
const addAccount = async (
|
||||
networkData: NetworksDataState,
|
||||
): Promise<Account | undefined> => {
|
||||
try {
|
||||
const namespaceChainId = `${networkData.namespace}:${networkData.chainId}`;
|
||||
const id = await getNextAccountId(namespaceChainId);
|
||||
const hdPath = getHDPath(namespaceChainId, `0'/0/${id}`);
|
||||
const accounts = await addAccountFromHDPath(hdPath, networkData);
|
||||
await updateAccountCounter(namespaceChainId, id);
|
||||
return accounts;
|
||||
} catch (error) {
|
||||
console.error('Error creating account:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const addAccountFromHDPath = async (
|
||||
hdPath: string,
|
||||
networkData: NetworksDataState,
|
||||
): Promise<Account | undefined> => {
|
||||
try {
|
||||
const account = await accountInfoFromHDPath(hdPath, networkData);
|
||||
if (!account) {
|
||||
throw new Error('Error while creating account');
|
||||
}
|
||||
|
||||
const { privKey, pubKey, address } = account;
|
||||
|
||||
const namespaceChainId = `${networkData.namespace}:${networkData.chainId}`;
|
||||
|
||||
const index = (await updateAccountIndices(namespaceChainId)).index;
|
||||
|
||||
await Promise.all([
|
||||
setInternetCredentials(
|
||||
`accounts/${namespaceChainId}/${index}`,
|
||||
'_',
|
||||
`${hdPath},${privKey},${pubKey},${address}`,
|
||||
),
|
||||
]);
|
||||
|
||||
return { index, pubKey, address, hdPath };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const storeNetworkData = async (
|
||||
networkData: NetworksFormData,
|
||||
): Promise<NetworksDataState[]> => {
|
||||
const networks = await getInternetCredentials('networks');
|
||||
let retrievedNetworks = [];
|
||||
if (networks) {
|
||||
retrievedNetworks = JSON.parse(networks!);
|
||||
}
|
||||
let networkId = 0;
|
||||
if (retrievedNetworks.length > 0) {
|
||||
networkId = retrievedNetworks[retrievedNetworks.length - 1].networkId + 1;
|
||||
}
|
||||
|
||||
const updatedNetworks: NetworksDataState[] = [
|
||||
...retrievedNetworks,
|
||||
{
|
||||
...networkData,
|
||||
networkId: String(networkId),
|
||||
},
|
||||
];
|
||||
await setInternetCredentials(
|
||||
'networks',
|
||||
'_',
|
||||
JSON.stringify(updatedNetworks),
|
||||
);
|
||||
return updatedNetworks;
|
||||
};
|
||||
|
||||
const retrieveNetworksData = async (): Promise<NetworksDataState[]> => {
|
||||
const networks = await getInternetCredentials('networks');
|
||||
|
||||
if(!networks){
|
||||
return [];
|
||||
}
|
||||
const parsedNetworks: NetworksDataState[] = JSON.parse(networks);
|
||||
return parsedNetworks;
|
||||
};
|
||||
|
||||
export const retrieveAccountsForNetwork = async (
|
||||
namespaceChainId: string,
|
||||
accountsIndices: string,
|
||||
): Promise<Account[]> => {
|
||||
const accountsIndexArray = accountsIndices.split(',');
|
||||
|
||||
const loadedAccounts = await Promise.all(
|
||||
accountsIndexArray.map(async i => {
|
||||
const { address, path, pubKey } = await getPathKey(
|
||||
namespaceChainId,
|
||||
Number(i),
|
||||
);
|
||||
|
||||
const account: Account = {
|
||||
index: Number(i),
|
||||
pubKey,
|
||||
address,
|
||||
hdPath: path,
|
||||
};
|
||||
return account;
|
||||
}),
|
||||
);
|
||||
|
||||
return loadedAccounts;
|
||||
};
|
||||
|
||||
const retrieveAccounts = async (
|
||||
currentNetworkData: NetworksDataState,
|
||||
): Promise<Account[] | undefined> => {
|
||||
const accountIndicesServer = await getInternetCredentials(
|
||||
`accountIndices/${currentNetworkData.namespace}:${currentNetworkData.chainId}`,
|
||||
);
|
||||
const accountIndices = accountIndicesServer;
|
||||
if (!accountIndices) {
|
||||
return;
|
||||
}
|
||||
const loadedAccounts = await retrieveAccountsForNetwork(
|
||||
`${currentNetworkData.namespace}:${currentNetworkData.chainId}`,
|
||||
accountIndices,
|
||||
)
|
||||
|
||||
return loadedAccounts;
|
||||
};
|
||||
|
||||
const retrieveSingleAccount = async (
|
||||
namespace: string,
|
||||
chainId: string,
|
||||
address: string,
|
||||
) => {
|
||||
let loadedAccounts;
|
||||
|
||||
const accountIndicesServer = await getInternetCredentials(
|
||||
`accountIndices/${namespace}:${chainId}`,
|
||||
);
|
||||
const accountIndices = accountIndicesServer;
|
||||
|
||||
if (!accountIndices) {
|
||||
throw new Error('Indices for given chain not found');
|
||||
}
|
||||
|
||||
loadedAccounts = await retrieveAccountsForNetwork(
|
||||
`${namespace}:${chainId}`,
|
||||
accountIndices,
|
||||
);
|
||||
|
||||
if (!loadedAccounts) {
|
||||
throw new Error('Accounts for given chain not found');
|
||||
}
|
||||
|
||||
return loadedAccounts.find(account => account.address === address);
|
||||
};
|
||||
|
||||
const resetWallet = async () => {
|
||||
try {
|
||||
await Promise.all([
|
||||
resetInternetCredentials('mnemonicServer'),
|
||||
resetKeyServers(EIP155),
|
||||
resetKeyServers(COSMOS),
|
||||
setInternetCredentials('networks', '_', JSON.stringify([])),
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Error resetting wallet:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const accountInfoFromHDPath = async (
|
||||
hdPath: string,
|
||||
networkData: NetworksDataState,
|
||||
): Promise<
|
||||
{ privKey: string; pubKey: string; address: string } | undefined
|
||||
> => {
|
||||
const mnemonicStore = await getInternetCredentials('mnemonicServer');
|
||||
if (!mnemonicStore) {
|
||||
throw new Error('Mnemonic not found!');
|
||||
}
|
||||
|
||||
const mnemonic = mnemonicStore;
|
||||
const hdNode = HDNode.fromMnemonic(mnemonic);
|
||||
const node = hdNode.derivePath(hdPath);
|
||||
|
||||
const privKey = node.privateKey;
|
||||
const pubKey = node.publicKey;
|
||||
|
||||
let address: string;
|
||||
|
||||
switch (networkData.namespace) {
|
||||
case EIP155:
|
||||
address = node.address;
|
||||
break;
|
||||
case COSMOS:
|
||||
address = (
|
||||
await getCosmosAccounts(mnemonic, hdPath, networkData.addressPrefix)
|
||||
).data.address;
|
||||
break;
|
||||
default:
|
||||
throw new Error('Invalid wallet type');
|
||||
}
|
||||
return { privKey, pubKey, address };
|
||||
};
|
||||
|
||||
const getNextAccountId = async (namespaceChainId: string): Promise<number> => {
|
||||
const idStore = await getInternetCredentials(
|
||||
`addAccountCounter/${namespaceChainId}`,
|
||||
);
|
||||
if (!idStore) {
|
||||
throw new Error('Account id not found');
|
||||
}
|
||||
|
||||
const accountCounter = idStore;
|
||||
const nextCounter = Number(accountCounter);
|
||||
return nextCounter;
|
||||
};
|
||||
|
||||
const updateAccountCounter = async (
|
||||
namespaceChainId: string,
|
||||
id: number,
|
||||
): Promise<void> => {
|
||||
const idStore = await getInternetCredentials(
|
||||
`addAccountCounter/${namespaceChainId}`,
|
||||
);
|
||||
if (!idStore) {
|
||||
throw new Error('Account id not found');
|
||||
}
|
||||
|
||||
const updatedCounter = String(id + 1);
|
||||
await resetInternetCredentials(`addAccountCounter/${namespaceChainId}`);
|
||||
await setInternetCredentials(
|
||||
`addAccountCounter/${namespaceChainId}`,
|
||||
'_',
|
||||
updatedCounter,
|
||||
);
|
||||
};
|
||||
|
||||
const getCosmosAccounts = async (
|
||||
mnemonic: string,
|
||||
path: string,
|
||||
prefix: string = COSMOS,
|
||||
): Promise<{ cosmosWallet: Secp256k1HdWallet; data: AccountData }> => {
|
||||
const cosmosWallet = await Secp256k1HdWallet.fromMnemonic(mnemonic, {
|
||||
hdPaths: [stringToPath(path)],
|
||||
prefix,
|
||||
});
|
||||
|
||||
const accountsData = await cosmosWallet.getAccounts();
|
||||
const data = accountsData[0];
|
||||
|
||||
return { cosmosWallet, data };
|
||||
};
|
||||
|
||||
export {
|
||||
createWallet,
|
||||
addAccount,
|
||||
addAccountFromHDPath,
|
||||
storeNetworkData,
|
||||
retrieveNetworksData,
|
||||
retrieveAccounts,
|
||||
retrieveSingleAccount,
|
||||
resetWallet,
|
||||
accountInfoFromHDPath,
|
||||
getNextAccountId,
|
||||
updateAccountCounter,
|
||||
getCosmosAccounts,
|
||||
};
|
36
src/utils/constants.ts
Normal file
36
src/utils/constants.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { COSMOS_TESTNET_CHAINS } from './wallet-connect/COSMOSData';
|
||||
import { EIP155_CHAINS } from './wallet-connect/EIP155Data';
|
||||
|
||||
export const EIP155 = 'eip155';
|
||||
export const COSMOS = 'cosmos';
|
||||
export const DEFAULT_NETWORKS = [
|
||||
{
|
||||
chainId: '1',
|
||||
networkName: EIP155_CHAINS['eip155:1'].name,
|
||||
namespace: EIP155,
|
||||
rpcUrl: EIP155_CHAINS['eip155:1'].rpc,
|
||||
blockExplorerUrl: '',
|
||||
currencySymbol: 'ETH',
|
||||
coinType: '60',
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
chainId: 'theta-testnet-001',
|
||||
networkName: COSMOS_TESTNET_CHAINS['cosmos:theta-testnet-001'].name,
|
||||
namespace: COSMOS,
|
||||
rpcUrl: COSMOS_TESTNET_CHAINS['cosmos:theta-testnet-001'].rpc,
|
||||
blockExplorerUrl: '',
|
||||
nativeDenom: 'uatom',
|
||||
addressPrefix: 'cosmos',
|
||||
coinType: '118',
|
||||
gasPrice: '0.025',
|
||||
isDefault: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const CHAINID_DEBOUNCE_DELAY = 250;
|
||||
|
||||
export const EMPTY_FIELD_ERROR = 'Field cannot be empty';
|
||||
export const INVALID_URL_ERROR = 'Invalid URL';
|
||||
|
||||
export const IS_NUMBER_REGEX = /^\d+$/;
|
17
src/utils/key-store.ts
Normal file
17
src/utils/key-store.ts
Normal file
@ -0,0 +1,17 @@
|
||||
const setInternetCredentials = (name:string, username:string, password:string) => {
|
||||
localStorage.setItem(name, password);
|
||||
};
|
||||
|
||||
const getInternetCredentials = (name:string) : string | null => {
|
||||
return localStorage.getItem(name);
|
||||
};
|
||||
|
||||
const resetInternetCredentials = (name:string) => {
|
||||
localStorage.removeItem(name);
|
||||
};
|
||||
|
||||
export {
|
||||
setInternetCredentials,
|
||||
getInternetCredentials,
|
||||
resetInternetCredentials
|
||||
}
|
158
src/utils/misc.ts
Normal file
158
src/utils/misc.ts
Normal file
@ -0,0 +1,158 @@
|
||||
/* Importing this library provides react native with a secure random source.
|
||||
For more information, "visit https://docs.ethers.org/v5/cookbook/react-native/#cookbook-reactnative-security" */
|
||||
import 'react-native-get-random-values';
|
||||
|
||||
import '@ethersproject/shims';
|
||||
|
||||
import {
|
||||
getInternetCredentials,
|
||||
resetInternetCredentials,
|
||||
setInternetCredentials,
|
||||
} from './key-store';
|
||||
|
||||
import { AccountData } from '@cosmjs/amino';
|
||||
import { DirectSecp256k1HdWallet } from '@cosmjs/proto-signing';
|
||||
import { stringToPath } from '@cosmjs/crypto';
|
||||
import { EIP155 } from './constants';
|
||||
import { NetworksDataState } from '../types';
|
||||
|
||||
const getMnemonic = async (): Promise<string> => {
|
||||
const mnemonicStore = await getInternetCredentials('mnemonicServer');
|
||||
if (!mnemonicStore) {
|
||||
throw new Error('Mnemonic not found!');
|
||||
}
|
||||
|
||||
const mnemonic = mnemonicStore;
|
||||
return mnemonic;
|
||||
};
|
||||
|
||||
const getHDPath = (namespaceChainId: string, path: string): string => {
|
||||
const namespace = namespaceChainId.split(':')[0];
|
||||
return namespace === EIP155 ? `m/44'/60'/${path}` : `m/44'/118'/${path}`;
|
||||
};
|
||||
|
||||
export const getDirectWallet = async (
|
||||
mnemonic: string,
|
||||
path: string,
|
||||
): Promise<{ directWallet: DirectSecp256k1HdWallet; data: AccountData }> => {
|
||||
const directWallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, {
|
||||
hdPaths: [stringToPath(`m/44'/118'/${path}`)],
|
||||
});
|
||||
const accountsData = await directWallet.getAccounts();
|
||||
const data = accountsData[0];
|
||||
|
||||
return { directWallet, data };
|
||||
};
|
||||
|
||||
const getPathKey = async (
|
||||
namespaceChainId: string,
|
||||
accountId: number,
|
||||
): Promise<{
|
||||
path: string;
|
||||
privKey: string;
|
||||
pubKey: string;
|
||||
address: string;
|
||||
}> => {
|
||||
const pathKeyStore = await getInternetCredentials(
|
||||
`accounts/${namespaceChainId}/${accountId}`,
|
||||
);
|
||||
|
||||
if (!pathKeyStore) {
|
||||
throw new Error('Error while fetching counter');
|
||||
}
|
||||
|
||||
const pathKeyVal = pathKeyStore;
|
||||
const pathkey = pathKeyVal.split(',');
|
||||
const path = pathkey[0];
|
||||
const privKey = pathkey[1];
|
||||
const pubKey = pathkey[2];
|
||||
const address = pathkey[3];
|
||||
|
||||
return { path, privKey, pubKey, address };
|
||||
};
|
||||
|
||||
const getAccountIndices = async (
|
||||
namespaceChainId: string,
|
||||
): Promise<{
|
||||
accountIndices: string;
|
||||
indices: number[];
|
||||
index: number;
|
||||
}> => {
|
||||
const counterStore = await getInternetCredentials(
|
||||
`accountIndices/${namespaceChainId}`,
|
||||
);
|
||||
|
||||
if (!counterStore) {
|
||||
throw new Error('Error while fetching counter');
|
||||
}
|
||||
|
||||
let accountIndices = counterStore;
|
||||
const indices = accountIndices.split(',').map(Number);
|
||||
const index = indices[indices.length - 1] + 1;
|
||||
|
||||
return { accountIndices, indices, index };
|
||||
};
|
||||
|
||||
const updateAccountIndices = async (
|
||||
namespaceChainId: string,
|
||||
): Promise<{ accountIndices: string; index: number }> => {
|
||||
const accountIndicesData = await getAccountIndices(namespaceChainId);
|
||||
const accountIndices = accountIndicesData.accountIndices;
|
||||
const index = accountIndicesData.index;
|
||||
const updatedAccountIndices = `${accountIndices},${index.toString()}`;
|
||||
|
||||
await resetInternetCredentials(`accountIndices/${namespaceChainId}`);
|
||||
await setInternetCredentials(
|
||||
`accountIndices/${namespaceChainId}`,
|
||||
'_',
|
||||
updatedAccountIndices,
|
||||
);
|
||||
|
||||
return { accountIndices: updatedAccountIndices, index };
|
||||
};
|
||||
|
||||
const resetKeyServers = async (namespace: string) => {
|
||||
const networksServer = await getInternetCredentials('networks');
|
||||
if (!networksServer) {
|
||||
throw new Error('Networks not found.');
|
||||
}
|
||||
|
||||
const networksData: NetworksDataState[] = JSON.parse(networksServer);
|
||||
const filteredNetworks = networksData.filter(
|
||||
(network: any) => network.namespace === namespace,
|
||||
);
|
||||
|
||||
if (filteredNetworks.length === 0) {
|
||||
throw new Error(`No networks found for namespace ${namespace}.`);
|
||||
}
|
||||
|
||||
filteredNetworks.forEach(async (network: any) => {
|
||||
const { chainId } = network;
|
||||
const namespaceChainId = `${namespace}:${chainId}`;
|
||||
|
||||
const idStore = await getInternetCredentials(
|
||||
`accountIndices/${namespaceChainId}`,
|
||||
);
|
||||
if (!idStore) {
|
||||
throw new Error(`Account indices not found for ${namespaceChainId}.`);
|
||||
}
|
||||
|
||||
const accountIds = idStore;
|
||||
const ids = accountIds.split(',').map(Number);
|
||||
const latestId = Math.max(...ids);
|
||||
|
||||
for (let i = 0; i <= latestId; i++) {
|
||||
await resetInternetCredentials(`accounts/${namespaceChainId}/${i}`);
|
||||
}
|
||||
await resetInternetCredentials(`addAccountCounter/${namespaceChainId}`);
|
||||
await resetInternetCredentials(`accountIndices/${namespaceChainId}`);
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
getMnemonic,
|
||||
getPathKey,
|
||||
updateAccountIndices,
|
||||
getHDPath,
|
||||
resetKeyServers,
|
||||
};
|
111
src/utils/sign-message.ts
Normal file
111
src/utils/sign-message.ts
Normal file
@ -0,0 +1,111 @@
|
||||
/* Importing this library provides react native with a secure random source.
|
||||
For more information, "visit https://docs.ethers.org/v5/cookbook/react-native/#cookbook-reactnative-security" */
|
||||
import 'react-native-get-random-values';
|
||||
|
||||
import '@ethersproject/shims';
|
||||
|
||||
import { Wallet } from 'ethers';
|
||||
import { SignDoc } from 'cosmjs-types/cosmos/tx/v1beta1/tx';
|
||||
|
||||
import { SignMessageParams } from '../types';
|
||||
import { getDirectWallet, getMnemonic, getPathKey } from './misc';
|
||||
import { getCosmosAccounts } from './accounts';
|
||||
import { COSMOS, EIP155 } from './constants';
|
||||
|
||||
const signMessage = async ({
|
||||
message,
|
||||
namespace,
|
||||
chainId,
|
||||
accountId,
|
||||
}: SignMessageParams): Promise<string | undefined> => {
|
||||
const path = await getPathKey(`${namespace}:${chainId}`, accountId);
|
||||
|
||||
switch (namespace) {
|
||||
case EIP155:
|
||||
return await signEthMessage(message, accountId, chainId);
|
||||
case COSMOS:
|
||||
return await signCosmosMessage(message, path.path);
|
||||
default:
|
||||
throw new Error('Invalid wallet type');
|
||||
}
|
||||
};
|
||||
|
||||
const signEthMessage = async (
|
||||
message: string,
|
||||
accountId: number,
|
||||
chainId: string,
|
||||
): Promise<string | undefined> => {
|
||||
try {
|
||||
const privKey = (await getPathKey(`${EIP155}:${chainId}`, accountId))
|
||||
.privKey;
|
||||
const wallet = new Wallet(privKey);
|
||||
const signature = await wallet.signMessage(message);
|
||||
|
||||
return signature;
|
||||
} catch (error) {
|
||||
console.error('Error signing Ethereum message:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const signCosmosMessage = async (
|
||||
message: string,
|
||||
path: string,
|
||||
): Promise<string | undefined> => {
|
||||
try {
|
||||
const mnemonic = await getMnemonic();
|
||||
const cosmosAccount = await getCosmosAccounts(mnemonic, path);
|
||||
const address = cosmosAccount.data.address;
|
||||
const cosmosSignature = await cosmosAccount.cosmosWallet.signAmino(
|
||||
address,
|
||||
{
|
||||
chain_id: '',
|
||||
account_number: '0',
|
||||
sequence: '0',
|
||||
fee: {
|
||||
gas: '0',
|
||||
amount: [],
|
||||
},
|
||||
msgs: [
|
||||
{
|
||||
type: 'sign/MsgSignData',
|
||||
value: {
|
||||
signer: address,
|
||||
data: btoa(message),
|
||||
},
|
||||
},
|
||||
],
|
||||
memo: '',
|
||||
},
|
||||
);
|
||||
|
||||
return cosmosSignature.signature.signature;
|
||||
} catch (error) {
|
||||
console.error('Error signing Cosmos message:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const signDirectMessage = async (
|
||||
namespaceChainId: string,
|
||||
accountId: number,
|
||||
signDoc: SignDoc,
|
||||
): Promise<string | undefined> => {
|
||||
try {
|
||||
const path = (await getPathKey(namespaceChainId, accountId)).path;
|
||||
const mnemonic = await getMnemonic();
|
||||
const { directWallet, data } = await getDirectWallet(mnemonic, path);
|
||||
|
||||
const directSignature = await directWallet.signDirect(
|
||||
data.address,
|
||||
signDoc,
|
||||
);
|
||||
|
||||
return directSignature.signature.signature;
|
||||
} catch (error) {
|
||||
console.error('Error signing Cosmos message:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export { signMessage, signEthMessage, signCosmosMessage, signDirectMessage };
|
42
src/utils/wallet-connect/COSMOSData.ts
Normal file
42
src/utils/wallet-connect/COSMOSData.ts
Normal file
@ -0,0 +1,42 @@
|
||||
// Taken from https://github.com/WalletConnect/web-examples/blob/main/advanced/wallets/react-wallet-v2/src/data/COSMOSData.ts
|
||||
|
||||
/**
|
||||
* Types
|
||||
*/
|
||||
export type TCosmosChain = keyof typeof COSMOS_TESTNET_CHAINS;
|
||||
|
||||
/**
|
||||
* Chains
|
||||
*/
|
||||
|
||||
// Added for pay.laconic.com
|
||||
export const COSMOS_TESTNET_CHAINS: Record<
|
||||
string,
|
||||
{
|
||||
chainId: string;
|
||||
name: string;
|
||||
rpc: string;
|
||||
namespace: string;
|
||||
}
|
||||
> = {
|
||||
'cosmos:theta-testnet-001': {
|
||||
chainId: 'theta-testnet-001',
|
||||
name: 'Cosmos Hub Testnet',
|
||||
rpc: 'https://rpc-t.cosmos.nodestake.top',
|
||||
namespace: 'cosmos',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Methods
|
||||
*/
|
||||
export const COSMOS_SIGNING_METHODS = {
|
||||
COSMOS_SIGN_DIRECT: 'cosmos_signDirect',
|
||||
COSMOS_SIGN_AMINO: 'cosmos_signAmino',
|
||||
};
|
||||
|
||||
export const COSMOS_METHODS = {
|
||||
...COSMOS_SIGNING_METHODS,
|
||||
COSMOS_SEND_TOKENS: 'cosmos_sendTokens', // Added for pay.laconic.com
|
||||
COSMOS_SEND_TRANSACTION: 'cosmos_sendTransaction', // Added for testnet onboarding app
|
||||
};
|
43
src/utils/wallet-connect/EIP155Data.ts
Normal file
43
src/utils/wallet-connect/EIP155Data.ts
Normal file
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @desc Refference list of eip155 chains
|
||||
* @url https://chainlist.org
|
||||
*/
|
||||
|
||||
// Taken from https://github.com/WalletConnect/web-examples/blob/main/advanced/wallets/react-wallet-v2/src/data/EIP155Data.ts
|
||||
|
||||
/**
|
||||
* Types
|
||||
*/
|
||||
export type TEIP155Chain = keyof typeof EIP155_CHAINS;
|
||||
|
||||
export type EIP155Chain = {
|
||||
chainId: number;
|
||||
name: string;
|
||||
logo: string;
|
||||
rgb: string;
|
||||
rpc: string;
|
||||
namespace: string;
|
||||
smartAccountEnabled?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Chains
|
||||
*/
|
||||
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',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Methods
|
||||
*/
|
||||
export const EIP155_SIGNING_METHODS = {
|
||||
PERSONAL_SIGN: 'personal_sign',
|
||||
ETH_SEND_TRANSACTION: 'eth_sendTransaction',
|
||||
};
|
36
src/utils/wallet-connect/WalletConnectUtils.tsx
Normal file
36
src/utils/wallet-connect/WalletConnectUtils.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import Config from 'react-native-config';
|
||||
|
||||
// import '@walletconnect/react-native-compat';
|
||||
import '@ethersproject/shims';
|
||||
// import { Core } from '@walletconnect/core';
|
||||
// import { ICore } from '@walletconnect/types';
|
||||
// import { Web3Wallet, IWeb3Wallet } from '@walletconnect/web3wallet';
|
||||
|
||||
export let web3wallet:
|
||||
// | IWeb3Wallet
|
||||
| any;
|
||||
export let core:
|
||||
// | ICore
|
||||
| any;
|
||||
|
||||
export async function createWeb3Wallet() {
|
||||
// core = new Core({
|
||||
// projectId: Config.WALLET_CONNECT_PROJECT_ID,
|
||||
// });
|
||||
|
||||
// web3wallet = await Web3Wallet.init({
|
||||
// core,
|
||||
// metadata: {
|
||||
// name: 'Laconic Wallet',
|
||||
// description: 'Laconic Wallet',
|
||||
// url: 'https://wallet.laconic.com/',
|
||||
// icons: ['https://avatars.githubusercontent.com/u/92608123'],
|
||||
// },
|
||||
// });
|
||||
}
|
||||
|
||||
export async function web3WalletPair(params: { uri: string }) {
|
||||
if (web3wallet) {
|
||||
// return await web3wallet.core.pairing.pair({ uri: params.uri });
|
||||
}
|
||||
}
|
4
src/utils/wallet-connect/common-data.ts
Normal file
4
src/utils/wallet-connect/common-data.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const NETWORK_METHODS = {
|
||||
GET_NETWORKS: 'getNetworks',
|
||||
CHANGE_NETWORK: 'changeNetwork',
|
||||
};
|
220
src/utils/wallet-connect/helpers.ts
Normal file
220
src/utils/wallet-connect/helpers.ts
Normal file
@ -0,0 +1,220 @@
|
||||
// Taken from https://medium.com/walletconnect/how-to-build-a-wallet-in-react-native-with-the-web3wallet-sdk-b6f57bf02f9a
|
||||
|
||||
import { utils } from 'ethers';
|
||||
|
||||
// import { ProposalTypes } from '@walletconnect/types';
|
||||
|
||||
import { Account, NetworksDataState } from '../../types';
|
||||
import { EIP155_SIGNING_METHODS } from './EIP155Data';
|
||||
import { mergeWith } from 'lodash';
|
||||
import { retrieveAccounts } from '../accounts';
|
||||
import { COSMOS, EIP155 } from '../constants';
|
||||
import { NETWORK_METHODS } from './common-data';
|
||||
import { COSMOS_METHODS } from './COSMOSData';
|
||||
|
||||
/**
|
||||
* Converts hex to utf8 string if it is valid bytes
|
||||
*/
|
||||
export function convertHexToUtf8(value: string) {
|
||||
if (utils.isHexString(value)) {
|
||||
return utils.toUtf8String(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets message from various signing request methods by filtering out
|
||||
* a value that is not an address (thus is a message).
|
||||
* If it is a hex string, it gets converted to utf8 string
|
||||
*/
|
||||
export function getSignParamsMessage(params: string[]) {
|
||||
const message = params.filter(p => !utils.isAddress(p))[0];
|
||||
|
||||
return convertHexToUtf8(message);
|
||||
}
|
||||
|
||||
export const getNamespaces = async (
|
||||
// optionalNamespaces: ProposalTypes.OptionalNamespaces,
|
||||
// requiredNamespaces: ProposalTypes.RequiredNamespaces,
|
||||
optionalNamespaces: any,
|
||||
requiredNamespaces: any,
|
||||
networksData: NetworksDataState[],
|
||||
selectedNetwork: NetworksDataState,
|
||||
accounts: Account[],
|
||||
currentIndex: number,
|
||||
) => {
|
||||
const namespaceChainId = `${selectedNetwork.namespace}:${selectedNetwork.chainId}`;
|
||||
|
||||
const combinedNamespaces = mergeWith(
|
||||
requiredNamespaces,
|
||||
optionalNamespaces,
|
||||
(obj, src) =>
|
||||
Array.isArray(obj) && Array.isArray(src) ? [...src, ...obj] : undefined,
|
||||
);
|
||||
|
||||
const walletConnectChains: string[] = [];
|
||||
|
||||
Object.keys(combinedNamespaces).forEach(key => {
|
||||
const { chains } = combinedNamespaces[key];
|
||||
|
||||
chains && walletConnectChains.push(...chains);
|
||||
});
|
||||
|
||||
// If combinedNamespaces is not empty, send back namespaces object based on requested chains
|
||||
// Else send back namespaces object using currently selected network
|
||||
if (Object.keys(combinedNamespaces).length > 0) {
|
||||
if (!(walletConnectChains.length > 0)) {
|
||||
return;
|
||||
}
|
||||
// Check for unsupported chains
|
||||
const networkChains = networksData.map(
|
||||
network => `${network.namespace}:${network.chainId}`,
|
||||
);
|
||||
if (!walletConnectChains.every(chain => networkChains.includes(chain))) {
|
||||
const unsupportedChains = walletConnectChains.filter(
|
||||
chain => !networkChains.includes(chain),
|
||||
);
|
||||
throw new Error(`Unsupported chains : ${unsupportedChains.join(',')}`);
|
||||
}
|
||||
|
||||
// Get required networks
|
||||
const requiredNetworks = networksData.filter(network =>
|
||||
walletConnectChains.includes(`${network.namespace}:${network.chainId}`),
|
||||
);
|
||||
// Get accounts for required networks
|
||||
const requiredAddressesPromise = requiredNetworks.map(
|
||||
async requiredNetwork => {
|
||||
const retrievedAccounts = await retrieveAccounts(requiredNetwork);
|
||||
|
||||
if (!retrievedAccounts) {
|
||||
throw new Error('Accounts for given network not found');
|
||||
}
|
||||
|
||||
const addresses = retrievedAccounts.map(
|
||||
retrieveAccount =>
|
||||
`${requiredNetwork.namespace}:${requiredNetwork.chainId}:${retrieveAccount.address}`,
|
||||
);
|
||||
|
||||
return addresses;
|
||||
},
|
||||
);
|
||||
|
||||
const requiredAddressesArray = await Promise.all(requiredAddressesPromise);
|
||||
const requiredAddresses = requiredAddressesArray.flat();
|
||||
|
||||
let sortedAccounts = requiredAddresses;
|
||||
|
||||
// If selected network is included in chains requested from dApp,
|
||||
// Put selected account as first account
|
||||
if (walletConnectChains.includes(namespaceChainId)) {
|
||||
const currentAddresses = requiredAddresses.filter(address =>
|
||||
address.includes(namespaceChainId),
|
||||
);
|
||||
sortedAccounts = [
|
||||
currentAddresses[currentIndex],
|
||||
...currentAddresses.filter((address, index) => index !== currentIndex),
|
||||
...requiredAddresses.filter(
|
||||
address => !currentAddresses.includes(address),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
// construct namespace object
|
||||
const newNamespaces = {
|
||||
eip155: {
|
||||
chains: walletConnectChains.filter(chain => chain.includes(EIP155)),
|
||||
// TODO: Debug optional namespace methods and events being required for approval
|
||||
methods: [
|
||||
...Object.values(EIP155_SIGNING_METHODS),
|
||||
...Object.values(NETWORK_METHODS),
|
||||
...(optionalNamespaces.eip155?.methods ?? []),
|
||||
...(requiredNamespaces.eip155?.methods ?? []),
|
||||
],
|
||||
events: [
|
||||
...(optionalNamespaces.eip155?.events ?? []),
|
||||
...(requiredNamespaces.eip155?.events ?? []),
|
||||
],
|
||||
accounts: sortedAccounts.filter(account => account.includes(EIP155)),
|
||||
},
|
||||
cosmos: {
|
||||
chains: walletConnectChains.filter(chain => chain.includes(COSMOS)),
|
||||
methods: [
|
||||
...Object.values(COSMOS_METHODS),
|
||||
...Object.values(NETWORK_METHODS),
|
||||
...(optionalNamespaces.cosmos?.methods ?? []),
|
||||
...(requiredNamespaces.cosmos?.methods ?? []),
|
||||
],
|
||||
events: [
|
||||
...(optionalNamespaces.cosmos?.events ?? []),
|
||||
...(requiredNamespaces.cosmos?.events ?? []),
|
||||
],
|
||||
accounts: sortedAccounts.filter(account => account.includes(COSMOS)),
|
||||
},
|
||||
};
|
||||
|
||||
return newNamespaces;
|
||||
} else {
|
||||
// Set selected account as the first account in supported namespaces
|
||||
const sortedAccounts = [
|
||||
accounts[currentIndex],
|
||||
...accounts.filter((account, index) => index !== currentIndex),
|
||||
];
|
||||
|
||||
switch (selectedNetwork.namespace) {
|
||||
case EIP155:
|
||||
return {
|
||||
eip155: {
|
||||
chains: [namespaceChainId],
|
||||
// TODO: Debug optional namespace methods and events being required for approval
|
||||
methods: [
|
||||
...Object.values(EIP155_SIGNING_METHODS),
|
||||
...Object.values(NETWORK_METHODS),
|
||||
...(optionalNamespaces.eip155?.methods ?? []),
|
||||
...(requiredNamespaces.eip155?.methods ?? []),
|
||||
],
|
||||
events: [
|
||||
...(optionalNamespaces.eip155?.events ?? []),
|
||||
...(requiredNamespaces.eip155?.events ?? []),
|
||||
],
|
||||
accounts: sortedAccounts.map(ethAccount => {
|
||||
return `${namespaceChainId}:${ethAccount.address}`;
|
||||
}),
|
||||
},
|
||||
cosmos: {
|
||||
chains: [],
|
||||
methods: [],
|
||||
events: [],
|
||||
accounts: [],
|
||||
},
|
||||
};
|
||||
case COSMOS:
|
||||
return {
|
||||
cosmos: {
|
||||
chains: [namespaceChainId],
|
||||
methods: [
|
||||
...Object.values(COSMOS_METHODS),
|
||||
...Object.values(NETWORK_METHODS),
|
||||
...(optionalNamespaces.cosmos?.methods ?? []),
|
||||
...(requiredNamespaces.cosmos?.methods ?? []),
|
||||
],
|
||||
events: [
|
||||
...(optionalNamespaces.cosmos?.events ?? []),
|
||||
...(requiredNamespaces.cosmos?.events ?? []),
|
||||
],
|
||||
accounts: sortedAccounts.map(cosmosAccount => {
|
||||
return `${namespaceChainId}:${cosmosAccount.address}`;
|
||||
}),
|
||||
},
|
||||
eip155: {
|
||||
chains: [],
|
||||
methods: [],
|
||||
events: [],
|
||||
accounts: [],
|
||||
},
|
||||
};
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
221
src/utils/wallet-connect/wallet-connect-requests.ts
Normal file
221
src/utils/wallet-connect/wallet-connect-requests.ts
Normal file
@ -0,0 +1,221 @@
|
||||
// Taken from https://medium.com/walletconnect/how-to-build-a-wallet-in-react-native-with-the-web3wallet-sdk-b6f57bf02f9a
|
||||
import { BigNumber, Wallet, providers } from 'ethers';
|
||||
|
||||
import { formatJsonRpcError, formatJsonRpcResult } from '@json-rpc-tools/utils';
|
||||
// import { SignClientTypes } from '@walletconnect/types';
|
||||
// import { getSdkError } from '@walletconnect/utils';
|
||||
import {
|
||||
SigningStargateClient,
|
||||
StdFee,
|
||||
MsgSendEncodeObject,
|
||||
} from '@cosmjs/stargate';
|
||||
import { EncodeObject } from '@cosmjs/proto-signing';
|
||||
import { LaconicClient } from '@cerc-io/registry-sdk';
|
||||
|
||||
import { EIP155_SIGNING_METHODS } from './EIP155Data';
|
||||
import { signDirectMessage, signEthMessage } from '../sign-message';
|
||||
import { Account } from '../../types';
|
||||
import { getMnemonic, getPathKey } from '../misc';
|
||||
import { getCosmosAccounts } from '../accounts';
|
||||
import { COSMOS_METHODS } from './COSMOSData';
|
||||
|
||||
interface EthSendTransaction {
|
||||
type: 'eth_sendTransaction';
|
||||
provider: providers.JsonRpcProvider;
|
||||
ethGasLimit: BigNumber;
|
||||
ethGasPrice: string | null;
|
||||
maxPriorityFeePerGas: BigNumber | null;
|
||||
maxFeePerGas: BigNumber | null;
|
||||
}
|
||||
|
||||
interface SignMessage {
|
||||
message: string;
|
||||
}
|
||||
interface EthPersonalSign extends SignMessage {
|
||||
type: 'personal_sign';
|
||||
}
|
||||
|
||||
interface CosmosSignDirect extends SignMessage {
|
||||
type: 'cosmos_signDirect';
|
||||
}
|
||||
|
||||
interface CosmosSignAmino extends SignMessage {
|
||||
type: 'cosmos_signAmino';
|
||||
}
|
||||
|
||||
interface CosmosSendTokens {
|
||||
type: 'cosmos_sendTokens';
|
||||
signingStargateClient: SigningStargateClient;
|
||||
cosmosFee: StdFee;
|
||||
sendMsg: MsgSendEncodeObject;
|
||||
memo: string;
|
||||
}
|
||||
|
||||
interface CosmosSendTransaction {
|
||||
type: 'cosmos_sendTransaction';
|
||||
LaconicClient: LaconicClient;
|
||||
cosmosFee: StdFee;
|
||||
txMsg: EncodeObject;
|
||||
}
|
||||
|
||||
export type WalletConnectRequests =
|
||||
| EthSendTransaction
|
||||
| EthPersonalSign
|
||||
| CosmosSignDirect
|
||||
| CosmosSignAmino
|
||||
| CosmosSendTokens
|
||||
| CosmosSendTransaction;
|
||||
|
||||
export async function approveWalletConnectRequest(
|
||||
// requestEvent: SignClientTypes.EventArguments['session_request'],
|
||||
requestEvent: any,
|
||||
account: Account,
|
||||
namespace: string,
|
||||
chainId: string,
|
||||
options: WalletConnectRequests,
|
||||
) {
|
||||
const { params, id } = requestEvent;
|
||||
const { request } = params;
|
||||
|
||||
const path = (await getPathKey(`${namespace}:${chainId}`, account.index))
|
||||
.path;
|
||||
const mnemonic = await getMnemonic();
|
||||
const cosmosAccount = await getCosmosAccounts(mnemonic, path);
|
||||
const address = account.address;
|
||||
|
||||
switch (request.method) {
|
||||
case EIP155_SIGNING_METHODS.ETH_SEND_TRANSACTION:
|
||||
if (!(options.type === 'eth_sendTransaction')) {
|
||||
throw new Error('Incorrect parameters passed');
|
||||
}
|
||||
|
||||
const privKey = (
|
||||
await getPathKey(`${namespace}:${chainId}`, account.index)
|
||||
).privKey;
|
||||
const wallet = new Wallet(privKey);
|
||||
const sendTransaction = request.params[0];
|
||||
const updatedTransaction =
|
||||
options.maxFeePerGas && options.maxPriorityFeePerGas
|
||||
? {
|
||||
...sendTransaction,
|
||||
gasLimit: options.ethGasLimit,
|
||||
maxFeePerGas: options.maxFeePerGas,
|
||||
maxPriorityFeePerGas: options.maxPriorityFeePerGas,
|
||||
type: 2,
|
||||
}
|
||||
: {
|
||||
...sendTransaction,
|
||||
gasLimit: options.ethGasLimit,
|
||||
gasPrice: options.ethGasPrice,
|
||||
type: 0,
|
||||
};
|
||||
|
||||
const connectedWallet = wallet.connect(options.provider);
|
||||
|
||||
const hash = await connectedWallet.sendTransaction(updatedTransaction);
|
||||
const receipt = typeof hash === 'string' ? hash : hash?.hash;
|
||||
return formatJsonRpcResult(id, {
|
||||
signature: receipt,
|
||||
});
|
||||
|
||||
case EIP155_SIGNING_METHODS.PERSONAL_SIGN:
|
||||
if (!(options.type === 'personal_sign')) {
|
||||
throw new Error('Incorrect parameters passed');
|
||||
}
|
||||
|
||||
const ethSignature = await signEthMessage(
|
||||
options.message,
|
||||
account.index,
|
||||
chainId,
|
||||
);
|
||||
return formatJsonRpcResult(id, ethSignature);
|
||||
|
||||
case COSMOS_METHODS.COSMOS_SIGN_DIRECT:
|
||||
// Reference: https://github.com/confio/cosmjs-types/blob/66e52711914fccd2a9d1a03e392d3628fdf499e2/src/cosmos/tx/v1beta1/tx.ts#L51
|
||||
// According above doc, in the signDoc interface 'bodyBytes' and 'authInfoBytes' have Uint8Array type
|
||||
if (!(options.type === 'cosmos_signDirect')) {
|
||||
throw new Error('Incorrect parameters passed');
|
||||
}
|
||||
|
||||
const bodyBytesArray = Uint8Array.from(
|
||||
Buffer.from(request.params.signDoc.bodyBytes, 'hex'),
|
||||
);
|
||||
const authInfoBytesArray = Uint8Array.from(
|
||||
Buffer.from(request.params.signDoc.authInfoBytes, 'hex'),
|
||||
);
|
||||
|
||||
const cosmosDirectSignature = await signDirectMessage(
|
||||
`${namespace}:${chainId}`,
|
||||
account.index,
|
||||
{
|
||||
...request.params.signDoc,
|
||||
bodyBytes: bodyBytesArray,
|
||||
authInfoBytes: authInfoBytesArray,
|
||||
},
|
||||
);
|
||||
|
||||
return formatJsonRpcResult(id, {
|
||||
signature: cosmosDirectSignature,
|
||||
});
|
||||
|
||||
case COSMOS_METHODS.COSMOS_SIGN_AMINO:
|
||||
if (!(options.type === 'cosmos_signAmino')) {
|
||||
throw new Error('Incorrect parameters passed');
|
||||
}
|
||||
|
||||
const cosmosAminoSignature = await cosmosAccount.cosmosWallet.signAmino(
|
||||
address,
|
||||
request.params.signDoc,
|
||||
);
|
||||
|
||||
if (!cosmosAminoSignature) {
|
||||
throw new Error('Error signing message');
|
||||
}
|
||||
|
||||
return formatJsonRpcResult(id, {
|
||||
signature: cosmosAminoSignature.signature.signature,
|
||||
});
|
||||
|
||||
case COSMOS_METHODS.COSMOS_SEND_TOKENS:
|
||||
if (!(options.type === 'cosmos_sendTokens')) {
|
||||
throw new Error('Incorrect parameters passed');
|
||||
}
|
||||
|
||||
const result = await options.signingStargateClient.signAndBroadcast(
|
||||
address,
|
||||
[options.sendMsg],
|
||||
options.cosmosFee,
|
||||
options.memo,
|
||||
);
|
||||
|
||||
return formatJsonRpcResult(id, {
|
||||
signature: result.transactionHash,
|
||||
});
|
||||
|
||||
case COSMOS_METHODS.COSMOS_SEND_TRANSACTION:
|
||||
if (!(options.type === 'cosmos_sendTransaction')) {
|
||||
throw new Error('Incorrect parameters passed');
|
||||
}
|
||||
const resultFromTx = await options.LaconicClient.signAndBroadcast(
|
||||
address,
|
||||
[options.txMsg],
|
||||
options.cosmosFee,
|
||||
);
|
||||
|
||||
return formatJsonRpcResult(id, {
|
||||
code: resultFromTx.code,
|
||||
});
|
||||
|
||||
default:
|
||||
// throw new Error(getSdkError('INVALID_METHOD').message);
|
||||
}
|
||||
}
|
||||
|
||||
export function rejectWalletConnectRequest(
|
||||
// request: SignClientTypes.EventArguments['session_request'],
|
||||
request: any,
|
||||
) {
|
||||
// const { id } = request;
|
||||
|
||||
// return formatJsonRpcError(id, getSdkError('USER_REJECTED_METHODS').message);
|
||||
}
|
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user