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