Feature/nym browser extension (#3637)
* Chore/browser extension bootstrap (#3257) * init package * set up TS and Webpack * add eslint config * add prettier config * add react and mui theme * add CI * update mui theme version number * Chore/browser extension routes (#3327) * start routes * create layouts * add initial app routes * add initial app pages * add global types * create reuseable components * move password and mnemonic fields to shared react components package * refactor register routes * move client address component to shared package * move components to ui folder * create menu and appbar components * adjust layout components * add readme * use memory router * Feature/nym browser extension login and send (#3373) * init package * set up TS and Webpack * add eslint config * add prettier config * add react and mui theme * add CI * update mui theme version number * Chore/browser extension routes (#3327) * start routes * create layouts * add initial app routes * add initial app pages * add global types * create reuseable components * move password and mnemonic fields to shared react components package * refactor register routes * move client address component to shared package * move components to ui folder * create menu and appbar components * adjust layout components * add readme * use memory router * add extension to mono-repo config * fix webpack build * util functions * add TX type * refactor routes * refactor pages + add send page * add page layout for app pages * set up app context * app components * set up connection config * fix lint errors * Chore/browser extension bootstrap (#3257) * init package * set up TS and Webpack * add eslint config * add prettier config * add react and mui theme * add CI * update mui theme version number * Chore/browser extension routes (#3327) * start routes * create layouts * add initial app routes * add initial app pages * add global types * create reuseable components * move password and mnemonic fields to shared react components package * refactor register routes * move client address component to shared package * move components to ui folder * create menu and appbar components * adjust layout components * add readme * use memory router * add extension to mono-repo config * util functions * add TX type * refactor routes * refactor pages + add send page * add page layout for app pages * set up app context * app components * set up connection config * use fee simulation when sending tokens * use object argument for simulate send api * login validation + fee refinements * use components from shared components lib * add receive modal (#3408) * account storage via wasm * method to get all storage keys * Feature/nym browser extension password encryption (single account) (#3442) * build wasm * reuse components and state for password pages * refactor registration pages * use login with password * import storage as local package * add yarn preinstall script to ts lint gh action * install wasm-pack for CI * use @nym scope for ext storage package * introduced a call to check if database was already initialised (#3465) * introduced a call to check if database was already initialised * use extension storage method to check for db existance --------- Co-authored-by: fmtabbara <fmtabbara@hotmail.co.uk> * introduced mnemonic key existence check (#3462) * Browser extension - Multi-accounts + view mnemonic action (#3488) * add UI for multi-accounts + add view mnemonic for accounts * refactor routes * set up import account * add account to existing wallet * check if account name exists before creating new one * handle password errors * add token to currency conversion * fixed ClientStorageError import path * fix CI * fix CI --------- Co-authored-by: Jędrzej Stuczyński <jedrzej.stuczynski@gmail.com>
This commit is contained in:
@@ -3,64 +3,70 @@ name: CI for linting Typescript
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'ts-packages/**'
|
||||
- 'sdk/typescript/**'
|
||||
- 'nym-connect/desktop/src/**'
|
||||
- 'nym-connect/desktop/package.json'
|
||||
- 'nym-connect/mobile/src/**'
|
||||
- 'nym-connect/mobile/package.json'
|
||||
- 'nym-wallet/src/**'
|
||||
- 'nym-wallet/package.json'
|
||||
- 'explorer/**'
|
||||
- "ts-packages/**"
|
||||
- "sdk/typescript/**"
|
||||
- "nym-connect/desktop/src/**"
|
||||
- "nym-connect/desktop/package.json"
|
||||
- "nym-connect/mobile/src/**"
|
||||
- "nym-connect/mobile/package.json"
|
||||
- "nym-wallet/src/**"
|
||||
- "nym-wallet/package.json"
|
||||
- "explorer/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- 'ts-packages/**'
|
||||
- 'sdk/typescript/**'
|
||||
- 'nym-connect/desktop/src/**'
|
||||
- 'nym-connect/desktop/package.json'
|
||||
- 'nym-connect/mobile/src/**'
|
||||
- 'nym-connect/mobile/package.json'
|
||||
- 'nym-wallet/src/**'
|
||||
- 'nym-wallet/package.json'
|
||||
- 'explorer/**'
|
||||
- "ts-packages/**"
|
||||
- "sdk/typescript/**"
|
||||
- "nym-connect/desktop/src/**"
|
||||
- "nym-connect/desktop/package.json"
|
||||
- "nym-connect/mobile/src/**"
|
||||
- "nym-connect/mobile/package.json"
|
||||
- "nym-wallet/src/**"
|
||||
- "nym-wallet/package.json"
|
||||
- "explorer/**"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: custom-runner-linux
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install rsync
|
||||
run: sudo apt-get install rsync
|
||||
continue-on-error: true
|
||||
- uses: rlespinasse/github-slug-action@v3.x
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Setup yarn
|
||||
run: npm install -g yarn
|
||||
- name: Install
|
||||
run: yarn
|
||||
- name: Build packages
|
||||
run: yarn build
|
||||
- name: Lint
|
||||
run: yarn lint && yarn tsc
|
||||
- name: Matrix - Node Install
|
||||
run: npm install
|
||||
working-directory: .github/workflows/support-files
|
||||
- name: Matrix - Send Notification
|
||||
env:
|
||||
NYM_NOTIFICATION_KIND: ts-packages
|
||||
NYM_PROJECT_NAME: "ts-packages"
|
||||
NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}"
|
||||
NYM_CI_WWW_LOCATION: "ts-${{ env.GITHUB_REF_SLUG }}"
|
||||
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
|
||||
GIT_BRANCH: "${GITHUB_REF##*/}"
|
||||
IS_SUCCESS: "${{ job.status == 'success' }}"
|
||||
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
|
||||
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM }}"
|
||||
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
|
||||
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
|
||||
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
|
||||
uses: docker://keybaseio/client:stable-node
|
||||
with:
|
||||
args: .github/workflows/support-files/notifications/entry_point.sh
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install rsync
|
||||
run: sudo apt-get install rsync
|
||||
continue-on-error: true
|
||||
- uses: rlespinasse/github-slug-action@v3.x
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install Rust stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
- name: Install wasm-pack
|
||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
- name: Setup yarn
|
||||
run: npm install -g yarn
|
||||
- name: Install
|
||||
run: yarn
|
||||
- name: Build packages
|
||||
run: yarn build
|
||||
- name: Lint
|
||||
run: yarn lint && yarn tsc
|
||||
- name: Matrix - Node Install
|
||||
run: npm install
|
||||
working-directory: .github/workflows/support-files
|
||||
- name: Matrix - Send Notification
|
||||
env:
|
||||
NYM_NOTIFICATION_KIND: ts-packages
|
||||
NYM_PROJECT_NAME: "ts-packages"
|
||||
NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}"
|
||||
NYM_CI_WWW_LOCATION: "ts-${{ env.GITHUB_REF_SLUG }}"
|
||||
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
|
||||
GIT_BRANCH: "${GITHUB_REF##*/}"
|
||||
IS_SUCCESS: "${{ job.status == 'success' }}"
|
||||
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
|
||||
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM }}"
|
||||
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
|
||||
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
|
||||
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
|
||||
uses: docker://keybaseio/client:stable-node
|
||||
with:
|
||||
args: .github/workflows/support-files/notifications/entry_point.sh
|
||||
|
||||
+1
-1
@@ -102,7 +102,7 @@ default-members = [
|
||||
"explorer-api",
|
||||
]
|
||||
|
||||
exclude = ["explorer", "contracts", "clients/webassembly", "nym-wallet", "nym-connect/mobile/src-tauri", "nym-connect/desktop", "cpu-cycles"]
|
||||
exclude = ["explorer", "contracts", "clients/webassembly", "nym-wallet", "nym-connect/mobile/src-tauri", "nym-connect/desktop", "nym-browser-extension/storage", "cpu-cycles"]
|
||||
|
||||
[workspace.package]
|
||||
authors = ["Nym Technologies SA"]
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 80 80"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_421_13045)">
|
||||
<path
|
||||
d="M40 80C62.0914 80 80 62.0914 80 40C80 17.9086 62.0914 0 40 0C17.9086 0 0 17.9086 0 40C0 62.0914 17.9086 80 40 80Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M40 77.2636C60.5801 77.2636 77.2636 60.5801 77.2636 40C77.2636 19.4199 60.5801 2.73645 40 2.73645C19.4199 2.73645 2.73645 19.4199 2.73645 40C2.73645 60.5801 19.4199 77.2636 40 77.2636Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M24.0224 32.471H23.9776V32.5084V45.5775L18.4673 32.4934L18.4598 32.471H18.4299H15.3047H11.7981H11.7607V32.5084V47.4916V47.529H11.7981H15.3047H15.3421V47.4916V34.4L20.8748 47.5065L20.8822 47.529H20.9121H24.0224H27.5215H27.5589V47.4916V32.5084V32.471H27.5215H24.0224Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M23.8965 32.39H27.64V47.6101H20.8238L20.7989 47.5352L15.4232 34.8006V47.6101H11.6797V32.39H18.5183L18.5432 32.4649L23.8965 45.1761V32.39ZM23.9776 45.5776L18.4673 32.4935L18.4598 32.471H11.7608V47.529H15.3421V34.4L20.8748 47.5065L20.8823 47.529H27.5589V32.471H23.9776V45.5776Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M68.2019 32.471H61.5178H61.4804L61.4729 32.5009L58.0486 45.6374L54.6169 32.5009L54.6094 32.471H54.5795H47.8804H47.8355V32.5084V47.4916V47.529H47.8804H51.3795H51.4169V47.4916V34.5047L54.8038 47.499L54.8112 47.529H54.8486H61.2337H61.2636L61.2711 47.499L64.658 34.5047V47.4916V47.529H64.6954H68.2019H68.2393V47.4916V32.5084V32.471H68.2019Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M61.4171 32.39H68.3204V47.6101H64.5769V35.1372L61.3497 47.5187L61.3495 47.5195L61.3269 47.6101H54.7479L54.7253 47.5195L54.7251 47.5187L51.4979 35.1372V47.6101H47.7545V32.39H54.6727L54.6953 32.4804L54.6955 32.4813L58.0485 45.3163L61.3943 32.4813L61.3945 32.4805L61.4171 32.39ZM58.0486 45.6374L54.6168 32.5009L54.6094 32.471H47.8355V47.529H51.4168V34.5047L54.8038 47.4991L54.8112 47.529H61.2636L61.2711 47.4991L64.658 34.5047V47.529H68.2393V32.471H61.4804L61.4729 32.5009L58.0486 45.6374Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M42.0711 32.471H42.0486L42.0412 32.486L37.7869 39.8804L33.5103 32.486L33.5028 32.471H33.4804H29.4355H29.3608L29.3982 32.5308L35.9851 43.9402V47.4916V47.529H36.0225H39.529H39.5664V47.4916V43.9402L46.1533 32.5308L46.1907 32.471H46.1159H42.0711Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M41.9985 32.39H46.337L46.2228 32.5726L39.6475 43.9619V47.6101H35.904V43.9619L29.3286 32.5726L29.2145 32.39H33.5529L33.5817 32.4475L37.7868 39.7181L41.9697 32.4476L41.9985 32.39ZM42.0411 32.486L37.7869 39.8804L33.5103 32.486L33.5028 32.471H29.3608L29.3981 32.5308L35.9851 43.9402V47.529H39.5664V43.9402L46.1533 32.5308L46.1907 32.471H42.0486L42.0411 32.486Z"
|
||||
fill="black"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_421_13045">
|
||||
<rect width="80" height="80" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -3,11 +3,11 @@
|
||||
"version": "0.19.0",
|
||||
"description": "A TypeScript client for interacting with smart contracts in Nym validators",
|
||||
"repository": "https://github.com/nymtech/nym",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "rollup -c ./rollup.config.mjs",
|
||||
"build:types": "rollup-type-bundler --dist ./dist/nym-validator-client",
|
||||
"build:types": "rollup-type-bundler --dist ./dist",
|
||||
"build:prod": "sh ./scripts/build-prod.sh",
|
||||
"test": "ts-mocha -p ./tsconfig.test.json ./src/tests/**/*.test.ts",
|
||||
"testmock": "ts-mocha -p ./tsconfig.test.json ./src/tests/mock/*.test.ts",
|
||||
@@ -29,16 +29,23 @@
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@cosmjs/cosmwasm-stargate": "^0.29.5",
|
||||
"@cosmjs/crypto": "^0.29.5",
|
||||
"@cosmjs/math": "^0.29.5",
|
||||
"@cosmjs/proto-signing": "^0.29.5",
|
||||
"@cosmjs/stargate": "^0.29.5",
|
||||
"@cosmjs/tendermint-rpc": "^0.29.5",
|
||||
"@favware/rollup-type-bundler": "^2.0.0",
|
||||
"@nymproject/types": "^1.0.0",
|
||||
"@rollup/plugin-commonjs": "^24.0.1",
|
||||
"@rollup/plugin-json": "^6.0.0",
|
||||
"@rollup/plugin-typescript": "^11.0.0",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
"rollup": "^3.17.2",
|
||||
"rollup-plugin-dts": "^5.2.0",
|
||||
"@rollup/plugin-typescript": "^11.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.7.0",
|
||||
"@typescript-eslint/parser": "^5.7.0",
|
||||
"axios": "^1.3.3",
|
||||
"cosmjs-types": "^0.4.1",
|
||||
"dotenv": "^16.0.3",
|
||||
"eslint": "^7.18.0",
|
||||
"eslint-config-airbnb": "^19.0.2",
|
||||
"eslint-config-airbnb-typescript": "^16.1.0",
|
||||
@@ -47,21 +54,15 @@
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-mocha": "^10.0.3",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"expect": "^28.1.3",
|
||||
"mocha": "^10.0.0",
|
||||
"moq.ts": "^7.3.4",
|
||||
"prettier": "^2.8.7",
|
||||
"rollup": "^3.17.2",
|
||||
"rollup-plugin-dts": "^5.2.0",
|
||||
"rollup-plugin-node-polyfills": "^0.2.1",
|
||||
"ts-mocha": "^10.0.0",
|
||||
"typedoc": "^0.22.13",
|
||||
"typescript": "^4.6.2",
|
||||
"cosmjs-types": "^0.4.1",
|
||||
"dotenv": "^16.0.3",
|
||||
"expect": "^28.1.3",
|
||||
"moq.ts": "^7.3.4",
|
||||
"@cosmjs/cosmwasm-stargate": "^0.29.5",
|
||||
"@cosmjs/crypto": "^0.29.5",
|
||||
"@cosmjs/math": "^0.29.5",
|
||||
"@cosmjs/proto-signing": "^0.29.5",
|
||||
"@cosmjs/stargate": "^0.29.5",
|
||||
"@cosmjs/tendermint-rpc": "^0.29.5",
|
||||
"axios": "^1.3.3"
|
||||
"typescript": "^4.6.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import nodePolyfills from 'rollup-plugin-node-polyfills';
|
||||
import json from '@rollup/plugin-json';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
|
||||
export default [
|
||||
{
|
||||
input: './src/index.ts',
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
dir: 'dist/nym-validator-client',
|
||||
dir: 'dist',
|
||||
format: 'cjs',
|
||||
},
|
||||
plugins: [resolve(), typescript(), commonjs(), json()],
|
||||
plugins: [nodePolyfills(), typescript(), commonjs(), json()],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -21,7 +21,7 @@ node ./scripts/buildPackageJson.mjs
|
||||
|
||||
# Copy README
|
||||
|
||||
cp README.md dist/nym-validator-client
|
||||
cp README.md dist/
|
||||
|
||||
# move the output outside of the yarn/npm workspaces
|
||||
|
||||
|
||||
@@ -17,4 +17,4 @@ const packageJson = {
|
||||
types,
|
||||
};
|
||||
|
||||
fs.writeFileSync('./dist/nym-validator-client/package.json', JSON.stringify(packageJson, null, 2));
|
||||
fs.writeFileSync('./dist/package.json', JSON.stringify(packageJson, null, 2));
|
||||
|
||||
@@ -45,7 +45,6 @@ import {
|
||||
} from '@nymproject/types';
|
||||
import QueryClient from './query-client';
|
||||
import SigningClient, { ISigningClient } from './signing-client';
|
||||
// import { DelegationBlock } from './types/shared';
|
||||
|
||||
export interface INymClient {
|
||||
readonly mixnetContract: string;
|
||||
@@ -626,7 +625,17 @@ export default class ValidatorClient implements INymClient {
|
||||
|
||||
// SIMULATE
|
||||
|
||||
public async simulateSend(signingAddress: string, from: string, to: string, amount: Coin[]) {
|
||||
return (this.client as SigningClient).simulateSend(signingAddress, from, to, amount);
|
||||
public async simulateSend({
|
||||
signingAddress,
|
||||
from,
|
||||
to,
|
||||
amount,
|
||||
}: {
|
||||
signingAddress: string;
|
||||
from: string;
|
||||
to: string;
|
||||
amount: Coin[];
|
||||
}) {
|
||||
return (this.client as ISigningClient).simulateSend(signingAddress, from, to, amount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,12 @@ describe('Simualtions', () => {
|
||||
});
|
||||
|
||||
it('can simulate sending tokens', async () => {
|
||||
const res = await client.simulateSend(client.address, client.address, client.address, [
|
||||
{ amount: '400000', denom: 'unym' },
|
||||
]);
|
||||
const res = await client.simulateSend({
|
||||
signingAddress: client.address,
|
||||
from: client.address,
|
||||
to: client.address,
|
||||
amount: [{ amount: '400000', denom: 'unym' }],
|
||||
});
|
||||
|
||||
expect(typeof res).toBe('number');
|
||||
}).timeout(10000);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/nym-validator-client",
|
||||
"outDir": "dist",
|
||||
"module": "ES2020",
|
||||
"target": "es2021",
|
||||
"allowJs": false,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::storage::errors::ClientStorageError;
|
||||
use crate::storage::error::ClientStorageError;
|
||||
use crate::topology::WasmTopologyError;
|
||||
use js_sys::Promise;
|
||||
use nym_client_core::config::GatewayEndpointConfig;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::client::config::Config;
|
||||
use crate::storage::errors::ClientStorageError;
|
||||
use crate::storage::error::ClientStorageError;
|
||||
use js_sys::Promise;
|
||||
use nym_client_core::client::base_client::storage::gateway_details::PersistedGatewayDetails;
|
||||
use nym_crypto::asymmetric::{encryption, identity};
|
||||
@@ -15,7 +15,7 @@ use wasm_utils::storage::{IdbVersionChangeEvent, WasmStorage};
|
||||
use wasm_utils::PromisableResult;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
pub(crate) mod errors;
|
||||
pub(crate) mod error;
|
||||
pub(crate) mod traits;
|
||||
|
||||
const STORAGE_NAME_PREFIX: &str = "wasm-client-storage";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::storage::errors::ClientStorageError;
|
||||
use crate::storage::error::ClientStorageError;
|
||||
use crate::storage::ClientStorage;
|
||||
use async_trait::async_trait;
|
||||
use nym_client_core::client::base_client::storage::gateway_details::{
|
||||
|
||||
@@ -22,6 +22,9 @@ pub enum StorageError {
|
||||
message: String,
|
||||
},
|
||||
|
||||
#[error("FATAL ERROR: storage key is somehow present {count} times in the table!")]
|
||||
DuplicateKey { count: u32 },
|
||||
|
||||
#[error("encountered issue with our storage encryption layer: {source}")]
|
||||
CryptoStorageError {
|
||||
#[from]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
use crate::console_log;
|
||||
use crate::storage::cipher_export::StoredExportedStoreCipher;
|
||||
use crate::storage::error::StorageError;
|
||||
use futures::TryFutureExt;
|
||||
use indexed_db_futures::IdbDatabase;
|
||||
use nym_store_cipher::{
|
||||
Aes256Gcm, Algorithm, EncryptedData, KdfInfo, KeySizeUser, Params, StoreCipher, Unsigned,
|
||||
@@ -87,6 +88,23 @@ impl WasmStorage {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn exists(db_name: &str) -> Result<bool, StorageError> {
|
||||
let db_req: OpenDbRequest = IdbDatabase::open(db_name)?;
|
||||
let db: IdbDatabase = db_req.into_future().await?;
|
||||
|
||||
// if the db was already created before, at the very least cipher info store should exist,
|
||||
// thus the iterator should return at least one value
|
||||
let some_stores_exist = db.object_store_names().next().is_some();
|
||||
|
||||
// that's super annoying - we have to do cleanup because opening db creates it
|
||||
// (if it didn't exist before)
|
||||
if !some_stores_exist {
|
||||
db.delete()?.into_future().await?
|
||||
}
|
||||
|
||||
Ok(some_stores_exist)
|
||||
}
|
||||
|
||||
pub fn serialize_value<T: Serialize>(&self, value: &T) -> Result<JsValue, StorageError> {
|
||||
if let Some(cipher) = &self.store_cipher {
|
||||
let encrypted = cipher.encrypt_json_value(value)?;
|
||||
@@ -134,6 +152,35 @@ impl WasmStorage {
|
||||
.store_value_raw(store, key, &self.serialize_value(&value)?)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn remove_value<K>(&self, store: &str, key: K) -> Result<(), StorageError>
|
||||
where
|
||||
K: wasm_bindgen::JsCast,
|
||||
{
|
||||
self.inner.remove_value_raw(store, key).await
|
||||
}
|
||||
|
||||
pub async fn has_value<K>(&self, store: &str, key: K) -> Result<bool, StorageError>
|
||||
where
|
||||
K: wasm_bindgen::JsCast,
|
||||
{
|
||||
match self.key_count(store, key).await? {
|
||||
n if n == 0 => Ok(false),
|
||||
n if n == 1 => Ok(true),
|
||||
n => Err(StorageError::DuplicateKey { count: n }),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn key_count<K>(&self, store: &str, key: K) -> Result<u32, StorageError>
|
||||
where
|
||||
K: wasm_bindgen::JsCast,
|
||||
{
|
||||
self.inner.get_key_count(store, key).await
|
||||
}
|
||||
|
||||
pub async fn get_all_keys(&self, store: &str) -> Result<js_sys::Array, StorageError> {
|
||||
self.inner.get_all_keys(store).await
|
||||
}
|
||||
}
|
||||
|
||||
struct IdbWrapper(IdbDatabase);
|
||||
@@ -169,6 +216,42 @@ impl IdbWrapper {
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn remove_value_raw<K>(&self, store: &str, key: K) -> Result<(), StorageError>
|
||||
where
|
||||
K: wasm_bindgen::JsCast,
|
||||
{
|
||||
self.0
|
||||
.transaction_on_one_with_mode(store, IdbTransactionMode::Readwrite)?
|
||||
.object_store(store)?
|
||||
.delete_owned(key)?
|
||||
.into_future()
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn get_key_count<K>(&self, store: &str, key: K) -> Result<u32, StorageError>
|
||||
where
|
||||
K: wasm_bindgen::JsCast,
|
||||
{
|
||||
self.0
|
||||
.transaction_on_one_with_mode(store, IdbTransactionMode::Readwrite)?
|
||||
.object_store(store)?
|
||||
.count_with_key_owned(key)?
|
||||
.into_future()
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn get_all_keys(&self, store: &str) -> Result<js_sys::Array, StorageError> {
|
||||
self.0
|
||||
.transaction_on_one_with_mode(store, IdbTransactionMode::Readonly)?
|
||||
.object_store(store)?
|
||||
.get_all_keys()?
|
||||
.into_future()
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn read_exported_cipher_store(
|
||||
&self,
|
||||
) -> Result<Option<StoredExportedStoreCipher>, StorageError> {
|
||||
|
||||
@@ -26,6 +26,6 @@ module.exports = mergeWithRules({
|
||||
crypto: false,
|
||||
net: false,
|
||||
zlib: false,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": [
|
||||
"@nymproject/eslint-config-react-typescript"
|
||||
],
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.eslint.json"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# environment
|
||||
.env.dev
|
||||
|
||||
# error logs
|
||||
yarn-error.log
|
||||
@@ -0,0 +1 @@
|
||||
16
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
RPC_URL=
|
||||
VALIDATOR_URL=
|
||||
PREFIX=
|
||||
MIXNET_CONTRACT_ADDRESS=
|
||||
VESTING_CONTRACT_ADDRESS=
|
||||
DENOM=
|
||||
BLOCK_EXPLORER_URL=
|
||||
@@ -0,0 +1,40 @@
|
||||
# Nym Browser Extension
|
||||
|
||||
The Nym browser extension lets you access your Nym wallet via the browser.
|
||||
|
||||
## Getting started
|
||||
|
||||
You will need:
|
||||
|
||||
- NodeJS (use `nvm install` to automatically install the correct version)
|
||||
- `npm`
|
||||
- `yarn`
|
||||
|
||||
> **Note**: This project is part of a mono repo, so you will need to build the shared packages before starting. And any time they change, you'll need to rebuild them.
|
||||
|
||||
From the [root of the repository](../README.md) run the following to build shared packages:
|
||||
|
||||
```
|
||||
yarn
|
||||
yarn build
|
||||
```
|
||||
|
||||
From the `nym-browser-extension` directory of the `nym` monorepo, run:
|
||||
|
||||
`yarn dev` to run the extension in dev mode.
|
||||
|
||||
You can then open a browser to http://localhost:9000 and start development.
|
||||
|
||||
OR
|
||||
|
||||
`yarn build` to build the extension.
|
||||
|
||||
The extension will build to the `nym-browser-extension/dist` directory.
|
||||
|
||||
## Load extension
|
||||
|
||||
To load the extension into a Chrome browser
|
||||
|
||||
- Go to `settings > extensions > manage extensions`
|
||||
- Select `Load unpacked`
|
||||
- Select the `nym-browser-extension/dist`
|
||||
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"name": "@nym/browser-extension",
|
||||
"version": "0.1.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"preinstall": "yarn build:wasm",
|
||||
"dev": "yarn webpack serve --config webpack.dev.js",
|
||||
"build": "yarn preinstall && webpack build --progress --config webpack.prod.js",
|
||||
"build:wasm": "cd storage && make wasm-pack",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"lint:ts": "tsc --noEmit",
|
||||
"storybook": "start-storybook -p 6006",
|
||||
"storybook:build": "build-storybook"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.7.0",
|
||||
"@emotion/styled": "^11.7.0",
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
"@mui/icons-material": "^5.11.11",
|
||||
"@mui/material": "^5.11.15",
|
||||
"@mui/system": "^5.11.15",
|
||||
"@nymproject/mui-theme": "^1.0.0",
|
||||
"@nymproject/nym-validator-client": "0.19.0",
|
||||
"@nymproject/react": "^1.0.0",
|
||||
"@nymproject/types": "^1.0.0",
|
||||
"@nymproject/extension-storage": "file:./storage/pkg",
|
||||
"@storybook/react": "^6.5.16",
|
||||
"big.js": "^6.2.1",
|
||||
"crypto-js": "^4.1.1",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^4.0.3",
|
||||
"react-hook-form": "^7.43.9",
|
||||
"react-router-dom": "^6.9.0",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nymproject/eslint-config-react-typescript": "^1.0.0",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
|
||||
"@svgr/webpack": "^6.1.1",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@types/big.js": "^6.1.6",
|
||||
"@types/crypto-js": "4.1.1",
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/node": "^18.16.1",
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@typescript-eslint/eslint-plugin": "^5.13.0",
|
||||
"@typescript-eslint/parser": "^5.13.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"dotenv-webpack": "^8.0.1",
|
||||
"eslint": "^8.10.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^16.1.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-root-import": "^1.0.4",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-jest": "^26.1.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-react": "^7.29.2",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"eslint-plugin-storybook": "^0.5.12",
|
||||
"favicons-webpack-plugin": "^5.0.2",
|
||||
"html-webpack-plugin": "^5.3.2",
|
||||
"jest": "^27.1.0",
|
||||
"mini-css-extract-plugin": "^2.2.2",
|
||||
"prettier": "^2.8.7",
|
||||
"react-refresh": "^0.14.0",
|
||||
"react-refresh-typescript": "^2.0.8",
|
||||
"style-loader": "^3.3.1",
|
||||
"ts-jest": "^27.0.5",
|
||||
"ts-loader": "^9.4.2",
|
||||
"tsconfig-paths-webpack-plugin": "^3.5.2",
|
||||
"typescript": "^4.6.2",
|
||||
"url-loader": "^4.1.1",
|
||||
"util": "^0.12.5",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^5.0.1",
|
||||
"webpack-dev-server": "^4.5.0",
|
||||
"webpack-favicons": "^1.3.8",
|
||||
"webpack-merge": "^5.8.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { NymBrowserExtThemeWithMode } from './theme/NymBrowserExtensionTheme';
|
||||
import { AppRoutes } from './routes';
|
||||
import { AppLayout } from './layouts/AppLayout';
|
||||
import { AppProvider } from './context';
|
||||
|
||||
export const App = () => (
|
||||
<NymBrowserExtThemeWithMode mode="light">
|
||||
<AppProvider>
|
||||
<AppLayout>
|
||||
<AppRoutes />
|
||||
</AppLayout>
|
||||
</AppProvider>
|
||||
</NymBrowserExtThemeWithMode>
|
||||
);
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { Avatar, ListItem, ListItemAvatar, ListItemButton, ListItemText } from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAppContext } from 'src/context';
|
||||
import { AccountActions } from './Actions';
|
||||
|
||||
const AccountItem = ({
|
||||
accountName,
|
||||
disabled,
|
||||
onSelect,
|
||||
}: {
|
||||
accountName: string;
|
||||
disabled: boolean;
|
||||
onSelect: () => void;
|
||||
}) => (
|
||||
<ListItem disableGutters disablePadding secondaryAction={<AccountActions accountName={accountName} />} divider>
|
||||
<ListItemButton onClick={onSelect} disabled={disabled}>
|
||||
<ListItemAvatar>
|
||||
<Avatar>{accountName[0]}</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={accountName} secondary={disabled && '(Selected)'} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
);
|
||||
|
||||
export const AccountList = () => {
|
||||
const navigate = useNavigate();
|
||||
const { accounts, selectAccount, selectedAccount } = useAppContext();
|
||||
|
||||
const handleSelectAccount = async (accountName: string) => {
|
||||
await selectAccount(accountName);
|
||||
navigate('/user/balance');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{accounts.map((accountName) => (
|
||||
<AccountItem
|
||||
disabled={selectedAccount === accountName}
|
||||
accountName={accountName}
|
||||
key={accountName}
|
||||
onSelect={() => handleSelectAccount(accountName)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { IconButton, ListItemIcon, ListItemText, Menu, MenuItem } from '@mui/material';
|
||||
import { MoreVert, VisibilityOutlined } from '@mui/icons-material';
|
||||
import { useAppContext } from 'src/context';
|
||||
|
||||
type ActionType = {
|
||||
title: string;
|
||||
Icon: React.ReactNode;
|
||||
onSelect: () => void;
|
||||
};
|
||||
|
||||
const ActionItem = ({ action }: { action: ActionType }) => (
|
||||
<MenuItem dense onClick={action.onSelect}>
|
||||
<ListItemIcon>{action.Icon}</ListItemIcon>
|
||||
<ListItemText>{action.title}</ListItemText>
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
export const AccountActions = ({ accountName }: { accountName: string }) => {
|
||||
const { setShowSeedForAccount } = useAppContext();
|
||||
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const actions: Array<ActionType> = [
|
||||
{
|
||||
title: 'View seed phrase',
|
||||
Icon: <VisibilityOutlined />,
|
||||
onSelect: () => {
|
||||
setShowSeedForAccount(accountName);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton onClick={handleClick}>
|
||||
<MoreVert />
|
||||
</IconButton>
|
||||
<Menu anchorEl={anchorEl} id="account-menu" open={open} onClose={handleClose} onClick={handleClose}>
|
||||
{actions.map((action) => (
|
||||
<ActionItem action={action} key={action.title} />
|
||||
))}
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Card, CardContent, Typography } from '@mui/material';
|
||||
import { PasswordInput } from '@nymproject/react/textfields/Password';
|
||||
import { ExtensionStorage } from '@nymproject/extension-storage';
|
||||
import { Button, ConfirmationModal } from 'src/components/ui';
|
||||
|
||||
const ShowSeedButton = ({ handleShowSeedPhrase }: { handleShowSeedPhrase: () => void }) => (
|
||||
<Button fullWidth variant="contained" onClick={handleShowSeedPhrase}>
|
||||
Show seed phrase
|
||||
</Button>
|
||||
);
|
||||
|
||||
const DoneButton = ({ onDone }: { onDone: () => void }) => (
|
||||
<Button fullWidth variant="contained" onClick={onDone}>
|
||||
Done
|
||||
</Button>
|
||||
);
|
||||
|
||||
const Seed = ({ seed }: { seed: string }) => (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography>{seed}</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
export const ViewSeedPhrase = ({ accountName, onDone }: { accountName: string; onDone: () => void }) => {
|
||||
const [seed, setSeed] = useState<string>();
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
const handleShowSeedPhrase = async () => {
|
||||
try {
|
||||
const storage = await new ExtensionStorage(password);
|
||||
const accountSeed = await storage.read_mnemonic(accountName);
|
||||
setSeed(accountSeed);
|
||||
} catch (e) {
|
||||
setError('Could not retrieve seed phrase. Please check your password');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfirmationModal
|
||||
open
|
||||
onClose={onDone}
|
||||
title={seed ? 'Account seed phrase' : 'Password'}
|
||||
subtitle={seed ? '' : 'Enter your account password'}
|
||||
ConfirmButton={
|
||||
seed ? <DoneButton onDone={onDone} /> : <ShowSeedButton handleShowSeedPhrase={handleShowSeedPhrase} />
|
||||
}
|
||||
>
|
||||
{seed ? (
|
||||
<Seed seed={seed} />
|
||||
) : (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
error={error}
|
||||
password={password}
|
||||
onUpdatePassword={(pw: string) => {
|
||||
setPassword(pw);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</ConfirmationModal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './Accounts';
|
||||
export * from './Actions';
|
||||
export * from './ViewSeedPhrase';
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
import { Stack } from '@mui/system';
|
||||
import { ClientAddress } from '@nymproject/react/client-address/ClientAddress';
|
||||
import { useAppContext } from 'src/context';
|
||||
|
||||
export const Address = () => {
|
||||
const { client } = useAppContext();
|
||||
|
||||
return (
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Typography fontWeight={700}>Address</Typography>
|
||||
<ClientAddress withCopy address={client?.address || ''} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Stack, Typography } from '@mui/material';
|
||||
import { useAppContext } from 'src/context';
|
||||
|
||||
export const Balance = () => {
|
||||
const { balance, fiatBalance, currency, getBalance } = useAppContext();
|
||||
|
||||
useEffect(() => {
|
||||
getBalance();
|
||||
}, []);
|
||||
|
||||
const fiat = fiatBalance ? `~ ${Intl.NumberFormat().format(fiatBalance)} ${currency.toUpperCase()}` : '-';
|
||||
|
||||
return (
|
||||
<Stack alignItems="center" gap={1}>
|
||||
<Typography sx={{ color: 'grey.600' }}>Available</Typography>
|
||||
<Typography variant="h4" textAlign="center">
|
||||
{balance} NYM
|
||||
</Typography>
|
||||
<Typography sx={{ color: 'grey.600' }}>{fiat}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './accounts';
|
||||
export * from './address';
|
||||
export * from './balance';
|
||||
export * from './receive';
|
||||
export * from './send';
|
||||
export * from './ui';
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { Card, CardContent, Dialog, DialogContent, DialogTitle, IconButton, Stack, Typography } from '@mui/material';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { useAppContext } from 'src/context';
|
||||
import { ClientAddress } from '@nymproject/react/client-address/ClientAddress';
|
||||
import { Close } from '@mui/icons-material';
|
||||
|
||||
export const ReceiveModal = ({ open, onClose }: { open: boolean; onClose: () => void }) => {
|
||||
const { client } = useAppContext();
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
|
||||
<DialogTitle>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography fontWeight={700}>Receive</Typography>
|
||||
<IconButton size="small" onClick={onClose} sx={{ padding: 0 }}>
|
||||
<Close fontSize="small" />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Stack gap={1} alignItems="center">
|
||||
<Card elevation={3} sx={{ my: 2, width: 200 }}>
|
||||
<CardContent sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<QRCodeSVG value={client?.address || ''} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Typography variant="body2" fontWeight={700}>
|
||||
Your Nym address
|
||||
</Typography>
|
||||
<ClientAddress address={client?.address || ''} withCopy smallIcons />
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ReceiveModal';
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
import { ConfirmationModal, Button } from 'src/components/ui';
|
||||
|
||||
export const SendConfirmationModal = ({
|
||||
amount,
|
||||
txUrl,
|
||||
onConfirm,
|
||||
}: {
|
||||
amount: string;
|
||||
txUrl: string;
|
||||
onConfirm: () => void;
|
||||
}) => (
|
||||
<ConfirmationModal
|
||||
open
|
||||
fullWidth
|
||||
title="You sent"
|
||||
ConfirmButton={
|
||||
<Button fullWidth variant="contained" size="large" onClick={onConfirm}>
|
||||
Done
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="h6">{amount}</Typography>
|
||||
<Link href={txUrl} target="_blank" sx={{ ml: 1 }} text="View on blockchain" />
|
||||
</Box>
|
||||
</ConfirmationModal>
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export * from './SendConfirmationModal';
|
||||
@@ -0,0 +1,14 @@
|
||||
import * as React from 'react';
|
||||
import { AppBar as MUIAppBar } from '@mui/material/';
|
||||
import Box from '@mui/material/Box';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
|
||||
export const AppBar = ({ Action }: { Action: React.ReactNode }) => (
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<MUIAppBar position="static" elevation={0} sx={{ bgcolor: 'rgba(103, 80, 164, 0.14)' }}>
|
||||
<Toolbar variant="dense">{Action}</Toolbar>
|
||||
</MUIAppBar>
|
||||
</Box>
|
||||
);
|
||||
|
||||
export default AppBar;
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ArrowBackIosRounded } from '@mui/icons-material';
|
||||
import { IconButton } from '@mui/material';
|
||||
|
||||
export const BackButton = ({ onBack }: { onBack?: () => void }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleClick = () => {
|
||||
if (onBack) {
|
||||
onBack();
|
||||
} else {
|
||||
navigate(-1);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return (
|
||||
<IconButton size="small" onClick={handleClick}>
|
||||
<ArrowBackIosRounded fontSize="small" />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Button as MUIButton, ButtonProps } from '@mui/material';
|
||||
|
||||
export const Button = (props: ButtonProps) => (
|
||||
<MUIButton {...props} disableElevation sx={{ textTransform: 'initial' }} />
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
import { NymLogoBW } from '@nymproject/react/logo/NymLogoBW';
|
||||
|
||||
export const Logo = ({ small }: { small?: boolean }) => (
|
||||
<NymLogoBW width={small ? '37.5px' : '75px'} height={small ? '37.5px' : '75px'} />
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { Stack, Typography } from '@mui/material';
|
||||
import { Logo } from '../Logo';
|
||||
import { Title } from '../Title';
|
||||
|
||||
export const LogoWithText = ({
|
||||
logoSmall,
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
logoSmall?: boolean;
|
||||
title: string;
|
||||
description?: string;
|
||||
}) => (
|
||||
<Stack alignItems="center" justifyContent="center" gap={3}>
|
||||
<Logo small={logoSmall} />
|
||||
<Title>{title}</Title>
|
||||
<Typography sx={{ color: 'grey.700', textAlign: 'center' }}>{description}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
@@ -0,0 +1,55 @@
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import { AccountBalanceWalletRounded, AccountCircleRounded, ArrowDownwardRounded } from '@mui/icons-material';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const menuSchema = [
|
||||
{
|
||||
title: 'Accounts',
|
||||
Icon: <AccountCircleRounded />,
|
||||
path: '/user/accounts',
|
||||
},
|
||||
{
|
||||
title: 'Balance',
|
||||
Icon: <AccountBalanceWalletRounded />,
|
||||
path: '/user/balance',
|
||||
},
|
||||
{
|
||||
title: 'Send',
|
||||
Icon: <ArrowDownwardRounded />,
|
||||
path: '/user/send',
|
||||
},
|
||||
];
|
||||
|
||||
export const MenuDrawer = ({ open, onClose }: { open: boolean; onClose: () => void }) => {
|
||||
const list = () => (
|
||||
<Box sx={{ width: 250 }} role="presentation" onClick={() => {}}>
|
||||
<List>
|
||||
{menuSchema.map(({ title, Icon, path }) => (
|
||||
<Link to={path} style={{ textDecoration: 'none', color: 'unset' }} key={title}>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton>
|
||||
<ListItemIcon>{Icon}</ListItemIcon>
|
||||
<ListItemText primary={title} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</Link>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Drawer anchor="left" open={open} onClose={onClose}>
|
||||
{list()}
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Breakpoint,
|
||||
Paper,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
SxProps,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { Button } from '../Button';
|
||||
|
||||
export interface ErrorModalProps {
|
||||
open: boolean;
|
||||
children?: React.ReactNode;
|
||||
title: React.ReactNode | string;
|
||||
subtitle?: React.ReactNode | string;
|
||||
sx?: SxProps;
|
||||
fullWidth?: boolean;
|
||||
maxWidth?: Breakpoint;
|
||||
backdropProps?: object;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const ErrorModal = ({
|
||||
open,
|
||||
onClose,
|
||||
children,
|
||||
title,
|
||||
subtitle,
|
||||
sx,
|
||||
fullWidth,
|
||||
maxWidth,
|
||||
backdropProps,
|
||||
}: ErrorModalProps) => {
|
||||
const Title = (
|
||||
<DialogTitle id="responsive-dialog-title" sx={{ pb: 2 }}>
|
||||
<Typography variant="body2" fontWeight={600}>
|
||||
{title}
|
||||
</Typography>
|
||||
{subtitle &&
|
||||
(typeof subtitle === 'string' ? (
|
||||
<Typography fontWeight={400} variant="subtitle1" fontSize={12} sx={{ color: 'grey.400' }}>
|
||||
{subtitle}
|
||||
</Typography>
|
||||
) : (
|
||||
subtitle
|
||||
))}
|
||||
</DialogTitle>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
aria-labelledby="responsive-dialog-title"
|
||||
maxWidth={maxWidth || 'sm'}
|
||||
sx={{ textAlign: 'center', ...sx }}
|
||||
fullWidth={fullWidth}
|
||||
BackdropProps={backdropProps}
|
||||
PaperComponent={Paper}
|
||||
PaperProps={{ elevation: 0 }}
|
||||
>
|
||||
{Title}
|
||||
<DialogContent>{children}</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 3 }}>
|
||||
<Button variant="contained" size="large" fullWidth onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { Box, CircularProgress, Modal, Stack, Typography, SxProps } from '@mui/material';
|
||||
|
||||
const modalStyle: SxProps = {
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 300,
|
||||
bgcolor: 'background.paper',
|
||||
boxShadow: 24,
|
||||
borderRadius: '16px',
|
||||
p: 4,
|
||||
};
|
||||
|
||||
export const LoadingModal = ({ sx, backdropProps }: { sx?: SxProps; backdropProps?: object }) => (
|
||||
<Modal open BackdropProps={backdropProps}>
|
||||
<Box sx={{ border: (t) => `1px solid ${t.palette.grey[500]}`, ...modalStyle, ...sx }} textAlign="center">
|
||||
<Stack spacing={4} direction="row" alignItems="center">
|
||||
<CircularProgress />
|
||||
<Typography sx={{ color: 'text.primary' }}>Please wait...</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Breakpoint,
|
||||
Paper,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
SxProps,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
|
||||
export interface ConfirmationModalProps {
|
||||
open: boolean;
|
||||
children?: React.ReactNode;
|
||||
title: React.ReactNode | string;
|
||||
subtitle?: React.ReactNode | string;
|
||||
ConfirmButton: React.ReactNode;
|
||||
sx?: SxProps;
|
||||
fullWidth?: boolean;
|
||||
maxWidth?: Breakpoint;
|
||||
backdropProps?: object;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const ConfirmationModal = ({
|
||||
open,
|
||||
onClose,
|
||||
children,
|
||||
title,
|
||||
subtitle,
|
||||
ConfirmButton,
|
||||
sx,
|
||||
fullWidth,
|
||||
maxWidth,
|
||||
backdropProps,
|
||||
}: ConfirmationModalProps) => {
|
||||
const Title = (
|
||||
<DialogTitle id="responsive-dialog-title" sx={{ pb: 2 }}>
|
||||
<Typography variant="body2" fontWeight={600}>
|
||||
{title}
|
||||
</Typography>
|
||||
{subtitle &&
|
||||
(typeof subtitle === 'string' ? (
|
||||
<Typography fontWeight={400} variant="subtitle1" fontSize={12} sx={{ color: 'grey.400' }}>
|
||||
{subtitle}
|
||||
</Typography>
|
||||
) : (
|
||||
subtitle
|
||||
))}
|
||||
</DialogTitle>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
aria-labelledby="responsive-dialog-title"
|
||||
maxWidth={maxWidth || 'sm'}
|
||||
sx={{ textAlign: 'center', ...sx }}
|
||||
fullWidth={fullWidth}
|
||||
BackdropProps={backdropProps}
|
||||
PaperComponent={Paper}
|
||||
PaperProps={{ elevation: 0 }}
|
||||
>
|
||||
{Title}
|
||||
<DialogContent>{children}</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 3 }}>{ConfirmButton}</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './Modal';
|
||||
export * from './LoadingModal';
|
||||
export * from './ErrorModal';
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
const FONT_WEIGHT = 400;
|
||||
|
||||
export const Title = ({ children }: { children: string }) => (
|
||||
<Typography variant="h5" fontWeight={FONT_WEIGHT}>
|
||||
{children}
|
||||
</Typography>
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
export * from './AppBar';
|
||||
export * from './Button';
|
||||
export * from './BackButton';
|
||||
export * from './Logo';
|
||||
export * from './LogoWithText';
|
||||
export * from './MenuDrawer';
|
||||
export * from './Modal';
|
||||
export * from './Title';
|
||||
@@ -0,0 +1,8 @@
|
||||
export const config = {
|
||||
rpcUrl: process.env.RPC_URL || '',
|
||||
validatorUrl: process.env.VALIDATOR_URL || '',
|
||||
prefix: process.env.PREFIX || '',
|
||||
mixnetContractAddress: process.env.MIXNET_CONTRACT_ADDRESS || '',
|
||||
vestingContractAddress: process.env.VESTING_CONTRACT_ADDRESS || '',
|
||||
denom: process.env.DENOM || '',
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import ValidatorClient from '@nymproject/nym-validator-client';
|
||||
import { ExtensionStorage } from '@nymproject/extension-storage';
|
||||
import { connectToValidator } from 'src/validator-client';
|
||||
import { unymToNym } from 'src/utils/coin';
|
||||
import { Currency, getTokenPrice } from 'src/utils/price';
|
||||
|
||||
type TAppContext = {
|
||||
client?: ValidatorClient;
|
||||
accounts: string[];
|
||||
balance?: string;
|
||||
fiatBalance?: number;
|
||||
denom: 'NYM';
|
||||
minorDenom: 'unym';
|
||||
currency: Currency;
|
||||
showSeedForAccount?: string;
|
||||
selectedAccount: string;
|
||||
storage?: ExtensionStorage;
|
||||
selectAccount: (accountName: string) => Promise<void>;
|
||||
setAccounts: (accounts: string[]) => void;
|
||||
setShowSeedForAccount: (accountName?: string) => void;
|
||||
handleUnlockWallet: (password: string) => void;
|
||||
getBalance: () => void;
|
||||
};
|
||||
|
||||
type TBalanceInNYMs = string;
|
||||
|
||||
const DEFAULT_ACCOUNT_NAME = 'Default account';
|
||||
|
||||
const AppContext = React.createContext({} as TAppContext);
|
||||
|
||||
export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [client, setClient] = useState<ValidatorClient>();
|
||||
const [selectedAccount, setSelected] = useState<string>(DEFAULT_ACCOUNT_NAME);
|
||||
const [balance, setBalance] = useState<TBalanceInNYMs>();
|
||||
const [fiatBalance, setFiatBalance] = useState<number>();
|
||||
const [accounts, setAccounts] = useState<string[]>([]);
|
||||
const [showSeedForAccount, setShowSeedForAccount] = useState<string>();
|
||||
const [storage, setStorage] = useState<ExtensionStorage>();
|
||||
|
||||
const denom = 'NYM';
|
||||
const minorDenom = 'unym';
|
||||
const currency = 'gbp';
|
||||
|
||||
const handleUnlockWallet = async (password: string) => {
|
||||
const store = await new ExtensionStorage(password);
|
||||
const mnemonic = await store.read_mnemonic(DEFAULT_ACCOUNT_NAME);
|
||||
const userAccounts = await store.get_all_mnemonic_keys();
|
||||
const clientFromMnemonic = await connectToValidator(mnemonic);
|
||||
|
||||
setStorage(store);
|
||||
setAccounts(userAccounts);
|
||||
setClient(clientFromMnemonic);
|
||||
};
|
||||
|
||||
const selectAccount = async (accountName: string) => {
|
||||
const mnemonic = await storage!.read_mnemonic(accountName);
|
||||
const clientFromMnemonic = await connectToValidator(mnemonic);
|
||||
setSelected(accountName);
|
||||
setClient(clientFromMnemonic);
|
||||
};
|
||||
|
||||
const getFiatBalance = async (bal: number) => {
|
||||
const tokenPrice = await getTokenPrice('nym', currency);
|
||||
const fiatBal = tokenPrice.nym.gbp * bal;
|
||||
return fiatBal;
|
||||
};
|
||||
|
||||
const getBalance = async () => {
|
||||
const bal = await client?.getBalance(client.address);
|
||||
if (bal) {
|
||||
const nym = unymToNym(Number(bal.amount));
|
||||
const fiat = await getFiatBalance(nym);
|
||||
setFiatBalance(fiat);
|
||||
setBalance(nym.toString());
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (client) {
|
||||
getBalance();
|
||||
}
|
||||
}, [client]);
|
||||
|
||||
const value = useMemo<TAppContext>(
|
||||
() => ({
|
||||
client,
|
||||
accounts,
|
||||
balance,
|
||||
fiatBalance,
|
||||
currency,
|
||||
denom,
|
||||
minorDenom,
|
||||
selectedAccount,
|
||||
storage,
|
||||
handleUnlockWallet,
|
||||
getBalance,
|
||||
setShowSeedForAccount,
|
||||
showSeedForAccount,
|
||||
setAccounts,
|
||||
selectAccount,
|
||||
}),
|
||||
[client, accounts, balance, fiatBalance, denom, minorDenom, selectedAccount, showSeedForAccount, storage],
|
||||
);
|
||||
|
||||
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
|
||||
};
|
||||
|
||||
export const useAppContext = () => React.useContext(AppContext);
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './app';
|
||||
export * from './send';
|
||||
export * from './register';
|
||||
@@ -0,0 +1,71 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { ExtensionStorage } from '@nymproject/extension-storage';
|
||||
|
||||
const RegisterContext = React.createContext({} as TRegisterContext);
|
||||
|
||||
type TRegisterContext = {
|
||||
userPassword: string;
|
||||
userMnemonic: string;
|
||||
accountName: string;
|
||||
checkAccountName: () => Promise<boolean>;
|
||||
setUserPassword: (password: string) => void;
|
||||
setUserMnemonic: (mnemonic: string) => void;
|
||||
setAccountName: (name: string) => void;
|
||||
createAccount: (args: { mnemonic: string; password: string; accName: string }) => Promise<void>;
|
||||
importAccount: () => Promise<string[]>;
|
||||
resetState: () => void;
|
||||
};
|
||||
|
||||
export const RegisterContextProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [userPassword, setUserPassword] = useState('');
|
||||
const [userMnemonic, setUserMnemonic] = useState('');
|
||||
const [accountName, setAccountName] = useState('');
|
||||
|
||||
const resetState = () => {
|
||||
setUserMnemonic('');
|
||||
setUserPassword('');
|
||||
setAccountName('');
|
||||
};
|
||||
|
||||
const createAccount = async ({
|
||||
mnemonic,
|
||||
password,
|
||||
accName,
|
||||
}: {
|
||||
mnemonic: string;
|
||||
password: string;
|
||||
accName: string;
|
||||
}) => {
|
||||
const storage = await new ExtensionStorage(password);
|
||||
await storage.store_mnemonic(accName, mnemonic);
|
||||
};
|
||||
|
||||
const importAccount = async () => {
|
||||
const storage = await new ExtensionStorage(userPassword);
|
||||
await storage.store_mnemonic(accountName, userMnemonic);
|
||||
const accounts = await storage.get_all_mnemonic_keys();
|
||||
return accounts;
|
||||
};
|
||||
|
||||
const checkAccountName = async () => true;
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
userPassword,
|
||||
setUserPassword,
|
||||
userMnemonic,
|
||||
accountName,
|
||||
setAccountName,
|
||||
setUserMnemonic,
|
||||
createAccount,
|
||||
checkAccountName,
|
||||
importAccount,
|
||||
resetState,
|
||||
}),
|
||||
[userPassword, userMnemonic, accountName],
|
||||
);
|
||||
|
||||
return <RegisterContext.Provider value={value}>{children}</RegisterContext.Provider>;
|
||||
};
|
||||
|
||||
export const useRegisterContext = () => React.useContext(RegisterContext);
|
||||
@@ -0,0 +1,112 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { DecCoin } from '@nymproject/types';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { nymToUnym } from 'src/utils/coin';
|
||||
import { TTransaction } from 'src/types';
|
||||
import { Fee, useGetFee } from 'src/hooks/useGetFee';
|
||||
import { createFeeObject } from 'src/utils/fee';
|
||||
import { useAppContext } from './app';
|
||||
|
||||
type TSendContext = {
|
||||
address?: string;
|
||||
amount?: DecCoin;
|
||||
transaction?: TTransaction;
|
||||
fee?: Fee;
|
||||
handleChangeAddress: (address?: string) => void;
|
||||
handleChangeAmount: (amount?: DecCoin) => void;
|
||||
handleSend: () => void;
|
||||
resetTx: () => void;
|
||||
onDone: () => void;
|
||||
handleGetFee: (address: string, amount: string) => Promise<void>;
|
||||
};
|
||||
|
||||
const SendContext = React.createContext({} as TSendContext);
|
||||
|
||||
export const SendProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [address, setAddress] = useState<string>();
|
||||
const [amount, setAmount] = useState<DecCoin>();
|
||||
const [transaction, setTransaction] = useState<TTransaction>();
|
||||
|
||||
const { client, minorDenom } = useAppContext();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleChangeAddress = (_address?: string) => setAddress(_address);
|
||||
|
||||
const handleChangeAmount = (_amount?: DecCoin) => setAmount(_amount);
|
||||
|
||||
const { getFee, fee } = useGetFee();
|
||||
|
||||
const handleGetFee = async (addressVal: string, amountVal: string) => {
|
||||
const unym = nymToUnym(Number(amountVal));
|
||||
|
||||
if (client) {
|
||||
// client loses its 'this' context when passing the method
|
||||
// TODO find a better way of doing this.
|
||||
getFee(client.simulateSend.bind(client), {
|
||||
signingAddress: client.address,
|
||||
from: client.address,
|
||||
to: addressVal,
|
||||
amount: [{ amount: unym.toString(), denom: minorDenom }],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSend = async () => {
|
||||
setTransaction({ status: 'loading', type: 'send' });
|
||||
let unyms;
|
||||
|
||||
if (!Number(amount?.amount)) {
|
||||
setTransaction({ status: 'error', type: 'send', message: 'Amount is not a valid number' });
|
||||
}
|
||||
|
||||
if (amount) {
|
||||
unyms = nymToUnym(Number(amount.amount));
|
||||
}
|
||||
|
||||
if (client && address && unyms) {
|
||||
try {
|
||||
const response = await client.send(
|
||||
address,
|
||||
[{ amount: unyms.toString(), denom: minorDenom }],
|
||||
createFeeObject(fee?.unym),
|
||||
);
|
||||
|
||||
setTransaction({ status: 'success', type: 'send', txHash: response?.transactionHash });
|
||||
} catch (e) {
|
||||
setTransaction({
|
||||
status: 'error',
|
||||
type: 'send',
|
||||
message: e instanceof Error ? e.message : 'Error making send transaction. Please try again',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resetTx = () => {
|
||||
setTransaction(undefined);
|
||||
};
|
||||
|
||||
const onDone = () => {
|
||||
navigate('/user/balance');
|
||||
};
|
||||
|
||||
const value = useMemo<TSendContext>(
|
||||
() => ({
|
||||
address,
|
||||
amount,
|
||||
transaction,
|
||||
fee,
|
||||
handleChangeAddress,
|
||||
handleChangeAmount,
|
||||
handleSend,
|
||||
resetTx,
|
||||
onDone,
|
||||
handleGetFee,
|
||||
}),
|
||||
[address, amount, transaction, fee],
|
||||
);
|
||||
|
||||
return <SendContext.Provider value={value}>{children}</SendContext.Provider>;
|
||||
};
|
||||
|
||||
export const useSendContext = () => React.useContext(SendContext);
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
export const useCreatePassword = () => {
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [isSafePassword, setIsSafePassword] = useState(false);
|
||||
const [hasReadTerms, setHasReadTerms] = useState(false);
|
||||
|
||||
const canProceed = isSafePassword && hasReadTerms && password === confirmPassword;
|
||||
|
||||
return {
|
||||
password,
|
||||
setPassword,
|
||||
confirmPassword,
|
||||
setConfirmPassword,
|
||||
setIsSafePassword,
|
||||
canProceed,
|
||||
setHasReadTerms,
|
||||
hasReadTerms,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import Big from 'big.js';
|
||||
import { useState } from 'react';
|
||||
import { unymToNym } from 'src/utils/coin';
|
||||
|
||||
export type Fee = { nym: number; unym: number };
|
||||
|
||||
export function useGetFee() {
|
||||
const [fee, setFee] = useState<Fee>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
async function getFee<T>(txReq: (args: T) => Promise<number | undefined>, args: T) {
|
||||
setError(undefined);
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const txFee = await txReq(args);
|
||||
|
||||
if (txFee) {
|
||||
const feeWithMultiplyer = Big(txFee).mul(1);
|
||||
console.log(fee);
|
||||
|
||||
const txFeeInNyms = unymToNym(feeWithMultiplyer);
|
||||
|
||||
setFee({ nym: Number(txFeeInNyms), unym: Number(feeWithMultiplyer) });
|
||||
}
|
||||
|
||||
if (!txFee) {
|
||||
setError('Unable to calculate fee');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setError(`Unable to get estimated fee: ${e}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return { fee, getFee, isLoading, error };
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Nym browser extension</title>
|
||||
</head>
|
||||
<body style="width: 360px; height: 600px">
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { App } from './App';
|
||||
|
||||
const rootDomElem = document.getElementById('root');
|
||||
|
||||
if (rootDomElem) {
|
||||
const root = createRoot(rootDomElem);
|
||||
root.render(<App />);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Container } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
export const AppLayout = ({ children }: { children: React.ReactNode }) => (
|
||||
<Container maxWidth="xs" disableGutters sx={{ height: '100vh' }}>
|
||||
{children}
|
||||
</Container>
|
||||
);
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { LogoWithText } from 'src/components/ui';
|
||||
|
||||
const layoutStyle = {
|
||||
height: '100%',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr',
|
||||
gridTemplateRows: 'repeat(3, 1fr)',
|
||||
gridColumnGap: '0px',
|
||||
gridRowGap: '0px',
|
||||
p: 2,
|
||||
};
|
||||
|
||||
export const CenteredLogoLayout = ({
|
||||
title,
|
||||
description,
|
||||
Actions,
|
||||
}: {
|
||||
title: string;
|
||||
description?: string;
|
||||
Actions: React.ReactNode;
|
||||
}) => (
|
||||
<Box sx={layoutStyle}>
|
||||
<Box />
|
||||
<LogoWithText title={title} description={description} />
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'flex-end' }}>{Actions}</Box>
|
||||
</Box>
|
||||
);
|
||||
@@ -0,0 +1,38 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Box, IconButton } from '@mui/material';
|
||||
import MenuIcon from '@mui/icons-material/Menu';
|
||||
import { AppBar, BackButton, MenuDrawer } from 'src/components/ui';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
const layoutStyle = {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr',
|
||||
gridTemplateRows: '50px 1fr',
|
||||
gridColumnGap: '0px',
|
||||
gridRowGap: '0px',
|
||||
};
|
||||
|
||||
export const PageLayout = ({ children, onBack }: { children: React.ReactNode; onBack?: () => void }) => {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
const MenuAction = useCallback(
|
||||
() => (
|
||||
<IconButton onClick={() => setMenuOpen(true)}>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const Action = location.pathname.includes('balance') ? MenuAction : BackButton;
|
||||
|
||||
return (
|
||||
<Box sx={layoutStyle}>
|
||||
<AppBar Action={<Action onBack={onBack} />} />
|
||||
<MenuDrawer open={menuOpen} onClose={() => setMenuOpen(false)} />
|
||||
<Box sx={{ p: 2 }}>{children}</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { BackButton, LogoWithText } from 'src/components/ui';
|
||||
|
||||
const layoutStyle = {
|
||||
height: '100%',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr',
|
||||
gridTemplaterows: '1fr 2fr 1fr',
|
||||
gridColumnGap: '0px',
|
||||
gridRowGap: '0px',
|
||||
position: 'relative',
|
||||
p: 2,
|
||||
};
|
||||
|
||||
export const TopLogoLayout = ({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
Actions,
|
||||
}: {
|
||||
title: string;
|
||||
description?: string;
|
||||
children: React.ReactNode;
|
||||
Actions: React.ReactNode;
|
||||
}) => (
|
||||
<Box sx={layoutStyle}>
|
||||
<Box sx={{ position: 'absolute', top: 16, left: 16 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'center' }}>
|
||||
<LogoWithText logoSmall title={title} description={description} />
|
||||
</Box>
|
||||
<Box>{children}</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-end', width: '100%' }}>{Actions}</Box>
|
||||
</Box>
|
||||
);
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './AppLayout';
|
||||
export * from './CenteredLogo';
|
||||
export * from './TopLogo';
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "Nym browser extension",
|
||||
"description": "Nym browser extension - Wallet & credentials",
|
||||
"version": "0.1.0",
|
||||
"manifest_version": 3,
|
||||
"action": {
|
||||
"default_popup": "index.html",
|
||||
"default_title": "Nym - Browser extension"
|
||||
},
|
||||
"icons": {
|
||||
"16": "favicon-16x16.png",
|
||||
"32": "favicon-32x32.png",
|
||||
"48": "favicon-48x48.png"
|
||||
},
|
||||
"content_security_policy": {
|
||||
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { PageLayout } from 'src/layouts/PageLayout';
|
||||
import { Stack } from '@mui/material';
|
||||
import { Add, ArrowDownward } from '@mui/icons-material';
|
||||
import { AccountList, Button } from 'src/components';
|
||||
import { ViewSeedPhrase } from 'src/components/accounts/ViewSeedPhrase';
|
||||
import { useAppContext, useRegisterContext } from 'src/context';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
export const Accounts = () => {
|
||||
const { showSeedForAccount, setShowSeedForAccount } = useAppContext();
|
||||
const { resetState } = useRegisterContext();
|
||||
|
||||
useEffect(() => {
|
||||
resetState();
|
||||
}, []);
|
||||
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleAddAccount = () => navigate(`${location.pathname}/add-account`);
|
||||
|
||||
const handleImportAccount = () => navigate(`${location.pathname}/import-account`);
|
||||
|
||||
const onBack = () => navigate('/user/balance');
|
||||
|
||||
return (
|
||||
<PageLayout onBack={onBack}>
|
||||
{showSeedForAccount && (
|
||||
<ViewSeedPhrase accountName={showSeedForAccount} onDone={() => setShowSeedForAccount(undefined)} />
|
||||
)}
|
||||
<AccountList />
|
||||
<Stack gap={1} alignItems="start" sx={{ mt: 2 }}>
|
||||
<Button startIcon={<Add />} onClick={handleAddAccount}>
|
||||
Add account
|
||||
</Button>
|
||||
<Button startIcon={<ArrowDownward />} onClick={handleImportAccount}>
|
||||
Import account
|
||||
</Button>
|
||||
</Stack>
|
||||
</PageLayout>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useRegisterContext } from 'src/context';
|
||||
import { SeedPhraseTemplate } from 'src/pages/templates';
|
||||
|
||||
export const AddAccount = () => {
|
||||
const { setUserMnemonic } = useRegisterContext();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onNext = (seedPhrase: string) => {
|
||||
setUserMnemonic(seedPhrase);
|
||||
navigate('/user/accounts/name-account');
|
||||
};
|
||||
|
||||
return <SeedPhraseTemplate onNext={onNext} />;
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { SetupCompleteTemplate } from 'src/pages/templates/Complete';
|
||||
|
||||
export const SetupComplete = () => {
|
||||
const navigate = useNavigate();
|
||||
const handleOnDone = () => {
|
||||
navigate('/user/accounts');
|
||||
};
|
||||
|
||||
return (
|
||||
<SetupCompleteTemplate title="You're all set!" description="Account successfully imported" onDone={handleOnDone} />
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import React, { useState } from 'react';
|
||||
import { PasswordInput } from '@nymproject/react/textfields/Password';
|
||||
import { Button } from 'src/components';
|
||||
import { useAppContext, useRegisterContext } from 'src/context';
|
||||
import { TopLogoLayout } from 'src/layouts';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export const ConfirmPassword = () => {
|
||||
const { setAccounts } = useAppContext();
|
||||
const { userPassword, setUserPassword, importAccount } = useRegisterContext();
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleOnComplete = async () => {
|
||||
try {
|
||||
const accounts = await importAccount();
|
||||
setAccounts(accounts);
|
||||
navigate('/user/accounts/complete');
|
||||
} catch (e) {
|
||||
setError('Incorrect password. Please try again');
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (password: string) => {
|
||||
setError(undefined);
|
||||
setUserPassword(password);
|
||||
};
|
||||
|
||||
return (
|
||||
<TopLogoLayout
|
||||
title="Confirm password"
|
||||
description="Confirm password to import account"
|
||||
Actions={
|
||||
<Button fullWidth variant="contained" size="large" onClick={handleOnComplete} disabled={!!error}>
|
||||
Confirm
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<PasswordInput value={userPassword} onUpdatePassword={onChange} error={error} />
|
||||
</TopLogoLayout>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useRegisterContext } from 'src/context/register';
|
||||
import { ImportAccountTemplate } from '../templates';
|
||||
|
||||
export const ImportAccount = () => {
|
||||
const { userMnemonic, setUserMnemonic } = useRegisterContext();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleOnNext = () => {
|
||||
navigate('/user/accounts/name-account');
|
||||
};
|
||||
|
||||
return (
|
||||
<ImportAccountTemplate userMnemonic={userMnemonic} onChangeUserMnemonic={setUserMnemonic} onNext={handleOnNext} />
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import React, { useState } from 'react';
|
||||
import { TextField } from '@mui/material';
|
||||
import { Button } from 'src/components';
|
||||
import { useRegisterContext } from 'src/context/register';
|
||||
import { TopLogoLayout } from 'src/layouts';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAppContext } from 'src/context';
|
||||
|
||||
export const NameAccount = () => {
|
||||
const { accountName, setAccountName } = useRegisterContext();
|
||||
const { storage } = useAppContext();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
const handleNext = async () => {
|
||||
const accountNameExists = await storage?.has_mnemonic(accountName);
|
||||
if (accountNameExists) {
|
||||
setError('Account name already exists. Please choose another account name');
|
||||
} else {
|
||||
navigate('/user/accounts/confirm-password');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TopLogoLayout
|
||||
title="Name account"
|
||||
description="Give your account a unique name"
|
||||
Actions={
|
||||
<Button fullWidth variant="contained" size="large" onClick={handleNext} disabled={!!error}>
|
||||
Next
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<TextField
|
||||
fullWidth
|
||||
value={accountName}
|
||||
onChange={(e) => {
|
||||
setError(undefined);
|
||||
setAccountName(e.target.value);
|
||||
}}
|
||||
error={!!error}
|
||||
helperText={error}
|
||||
/>
|
||||
</TopLogoLayout>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './Accounts';
|
||||
export * from './AddAccount';
|
||||
export * from './Complete';
|
||||
export * from './ConfirmPassword';
|
||||
export * from './ImportAccount';
|
||||
export * from './NameAccount';
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
import { Box } from '@mui/system';
|
||||
import { TopLogoLayout } from 'src/layouts';
|
||||
|
||||
const steps = [
|
||||
'Make sure you have your mnemonic saved',
|
||||
'Uninstal Nym extension wallet',
|
||||
'Reinstal Nym extension wallet',
|
||||
'Import your account using seed phrase',
|
||||
'Create new password',
|
||||
];
|
||||
|
||||
export const ForgotPassword = () => (
|
||||
<TopLogoLayout title="Forgot password" description="Follow instructions below" Actions={<div />}>
|
||||
<Box sx={{ px: 2 }}>
|
||||
{steps.map((step, index) => (
|
||||
<Typography sx={{ color: 'grey.700', my: 3 }} key={step} variant="body2">
|
||||
{`${index + 1}. ${step}`}
|
||||
</Typography>
|
||||
))}
|
||||
</Box>
|
||||
</TopLogoLayout>
|
||||
);
|
||||
@@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import { Stack, TextField } from '@mui/material';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { Button } from 'src/components/ui';
|
||||
import { CenteredLogoLayout } from 'src/layouts/CenteredLogo';
|
||||
import { useAppContext } from 'src/context';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { validationSchema } from './validationSchema';
|
||||
|
||||
export const Login = () => {
|
||||
const { handleUnlockWallet } = useAppContext();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm({ resolver: zodResolver(validationSchema), defaultValues: { password: '' } });
|
||||
|
||||
const onSubmit = async (data: { password: string }) => {
|
||||
try {
|
||||
await handleUnlockWallet(data.password);
|
||||
} catch (e) {
|
||||
setError('password', { message: 'Incorrect password. Please try again.' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<CenteredLogoLayout
|
||||
title="Privacy crypto wallet"
|
||||
Actions={
|
||||
<Stack gap={1} width="100%" justifyContent="flex-end">
|
||||
<TextField
|
||||
{...register('password')}
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
sx={{ mb: 3 }}
|
||||
helperText={errors.password?.message}
|
||||
error={!!errors.password}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
disabled={isSubmitting}
|
||||
variant="contained"
|
||||
disableElevation
|
||||
size="large"
|
||||
fullWidth
|
||||
>
|
||||
{isSubmitting ? 'Loading..' : 'Unlock'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
disableElevation
|
||||
size="large"
|
||||
fullWidth
|
||||
color="primary"
|
||||
onClick={() => navigate(`${location.pathname}/forgot-password`)}
|
||||
>
|
||||
Forgot password?
|
||||
</Button>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './Login';
|
||||
export * from './ForgotPassword';
|
||||
@@ -0,0 +1,5 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
export const validationSchema = z.object({
|
||||
password: z.string().min(1, { message: 'Required' }),
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Box, Stack, IconButton, Typography } from '@mui/material';
|
||||
import { ArrowDownwardRounded, ArrowUpwardRounded, TollRounded } from '@mui/icons-material';
|
||||
import { PageLayout } from 'src/layouts/PageLayout';
|
||||
import { Address, Balance, ReceiveModal } from 'src/components';
|
||||
|
||||
type ActionsSchema = Array<{
|
||||
title: string;
|
||||
Icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
}>;
|
||||
|
||||
const Actions = ({ actionsSchema }: { actionsSchema: ActionsSchema }) => (
|
||||
<Box display="flex" justifyContent="space-evenly">
|
||||
{actionsSchema.map(({ title, Icon, onClick }) => (
|
||||
<Stack justifyContent="center" alignItems="center" key={title}>
|
||||
<IconButton color="primary" size="large" onClick={onClick}>
|
||||
{Icon}
|
||||
</IconButton>
|
||||
<Typography>{title}</Typography>
|
||||
</Stack>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const BalancePage = () => {
|
||||
const [showReceiveModal, setShowReceiveModal] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const actionsSchema = [
|
||||
{
|
||||
title: 'Send',
|
||||
Icon: <ArrowDownwardRounded fontSize="large" />,
|
||||
onClick: () => navigate('/user/send'),
|
||||
},
|
||||
{
|
||||
title: 'Receive',
|
||||
Icon: <ArrowUpwardRounded fontSize="large" />,
|
||||
onClick: () => setShowReceiveModal(true),
|
||||
},
|
||||
{
|
||||
title: 'Buy',
|
||||
Icon: <TollRounded fontSize="large" />,
|
||||
onClick: () => navigate('/user/balance'),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<Stack gap={6}>
|
||||
<ReceiveModal open={showReceiveModal} onClose={() => setShowReceiveModal(false)} />
|
||||
<Address />
|
||||
<Balance />
|
||||
<Actions actionsSchema={actionsSchema} />
|
||||
</Stack>
|
||||
</PageLayout>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import React from 'react';
|
||||
|
||||
export const Delegation = () => <h1>Delegation</h1>;
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { Stack } from '@mui/system';
|
||||
import { Button } from 'src/components/ui';
|
||||
import { CenteredLogoLayout } from 'src/layouts';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export const Home = () => (
|
||||
<CenteredLogoLayout
|
||||
title="Welcome to Nym"
|
||||
Actions={
|
||||
<Stack gap={2} width="100%" justifyContent="flex-end">
|
||||
<Link to="/register/create-password" style={{ textDecoration: 'none' }}>
|
||||
<Button variant="contained" disableElevation size="large" fullWidth>
|
||||
Create new account
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/register/import-account" style={{ textDecoration: 'none' }}>
|
||||
<Button variant="text" disableElevation size="large" fullWidth color="primary">
|
||||
Import existing account
|
||||
</Button>
|
||||
</Link>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
export * from './accounts';
|
||||
export * from './auth';
|
||||
export * from './balance';
|
||||
export * from './home';
|
||||
export * from './receive';
|
||||
export * from './send';
|
||||
export * from './settings';
|
||||
export * from './delegation';
|
||||
@@ -0,0 +1,3 @@
|
||||
import React from 'react';
|
||||
|
||||
export const Receive = () => <h1>Receive</h1>;
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import { SetupCompleteTemplate } from '../templates/Complete';
|
||||
|
||||
export const SetupComplete = ({ onDone }: { onDone: () => void }) => (
|
||||
<SetupCompleteTemplate
|
||||
title="You're all set!"
|
||||
description="Open the extension and sign in to begin your interchain journey"
|
||||
onDone={onDone}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { useCreatePassword } from 'src/hooks/useCreatePassword';
|
||||
import { useRegisterContext } from 'src/context/register';
|
||||
import { CreatePasswordTemplate } from 'src/pages/templates/CreatePassword';
|
||||
|
||||
export const CreatePasswordOnExistingAccount = ({ onComplete }: { onComplete: () => void }) => {
|
||||
const passwordState = useCreatePassword();
|
||||
const { createAccount, userMnemonic } = useRegisterContext();
|
||||
|
||||
const handleOnComplete = async () => {
|
||||
await createAccount({ mnemonic: userMnemonic, password: passwordState.password, accName: 'Default account' });
|
||||
onComplete();
|
||||
};
|
||||
|
||||
return <CreatePasswordTemplate {...passwordState} onNext={handleOnComplete} />;
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { useCreatePassword } from 'src/hooks/useCreatePassword';
|
||||
import { useRegisterContext } from 'src/context/register';
|
||||
import { CreatePasswordTemplate } from 'src/pages/templates/CreatePassword';
|
||||
|
||||
export const CreatePasswordOnNewAccount = ({ onNext }: { onNext: () => void }) => {
|
||||
const passwordState = useCreatePassword();
|
||||
const { setUserPassword } = useRegisterContext();
|
||||
|
||||
const handleCreateAccount = async () => {
|
||||
await setUserPassword(passwordState.password);
|
||||
onNext();
|
||||
};
|
||||
|
||||
return <CreatePasswordTemplate {...passwordState} onNext={handleCreateAccount} />;
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useRegisterContext } from 'src/context/register';
|
||||
import { ImportAccountTemplate } from '../templates/ImportAccount';
|
||||
|
||||
export const ImportAccount = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const { setUserMnemonic, userMnemonic } = useRegisterContext();
|
||||
|
||||
const handleNext = async () => {
|
||||
navigate(`${location.pathname}/create-password`);
|
||||
};
|
||||
|
||||
return (
|
||||
<ImportAccountTemplate userMnemonic={userMnemonic} onChangeUserMnemonic={setUserMnemonic} onNext={handleNext} />
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useRegisterContext } from 'src/context/register';
|
||||
import { SeedPhraseTemplate } from '../templates/SeedPhrase';
|
||||
|
||||
export const SeedPhrase = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { createAccount, userPassword } = useRegisterContext();
|
||||
|
||||
const handleEncryptSeedPhrase = async (seedPhrase: string) => {
|
||||
await createAccount({ mnemonic: seedPhrase, password: userPassword, accName: 'Default account' });
|
||||
navigate('/register/complete');
|
||||
};
|
||||
|
||||
return <SeedPhraseTemplate onNext={handleEncryptSeedPhrase} />;
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './Complete';
|
||||
export * from './CreatePasswordOnExistingAccount';
|
||||
export * from './CreatePasswordOnNewAccount';
|
||||
export * from './ImportAccount';
|
||||
export * from './SeedPhrase';
|
||||
@@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import { Box, Divider, ListItem, ListItemText, Stack, Typography } from '@mui/material';
|
||||
import { Button } from 'src/components';
|
||||
import { PageLayout } from 'src/layouts/PageLayout';
|
||||
import { useAppContext, useSendContext } from 'src/context';
|
||||
import { ErrorModal, LoadingModal } from 'src/components/ui/Modal';
|
||||
import { SendConfirmationModal } from 'src/components/send/SendConfirmationModal';
|
||||
import { blockExplorerUrl } from 'src/urls';
|
||||
|
||||
const InfoItem = ({ label, value }: { label: string; value: string }) => (
|
||||
<Box>
|
||||
<ListItem disableGutters disablePadding>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography fontSize="small" fontWeight={600}>
|
||||
{label}
|
||||
</Typography>
|
||||
}
|
||||
secondary={
|
||||
<Typography fontSize="small" fontWeight={600}>
|
||||
{value}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider sx={{ my: 1 }} />
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const SendConfirmationPage = ({ onCancel }: { onCancel: () => void }) => {
|
||||
const { client, denom } = useAppContext();
|
||||
const { address, amount, fee, handleSend, transaction, resetTx, onDone } = useSendContext();
|
||||
|
||||
const calculateTotal = () => (Number(fee?.nym) + Number(amount?.amount)).toString();
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
{transaction?.status === 'success' && (
|
||||
<SendConfirmationModal
|
||||
amount={`${amount?.amount} ${denom}`}
|
||||
txUrl={`${blockExplorerUrl}/transactions/${transaction.txHash}`}
|
||||
onConfirm={onDone}
|
||||
/>
|
||||
)}
|
||||
{transaction?.status === 'loading' && <LoadingModal />}
|
||||
{transaction?.status === 'error' && (
|
||||
<ErrorModal open title="Transaction failed" onClose={resetTx}>
|
||||
<Typography>{transaction.message}</Typography>
|
||||
</ErrorModal>
|
||||
)}
|
||||
<Stack gap={1} height="100%">
|
||||
<InfoItem label="From" value={client?.address || ''} />
|
||||
<InfoItem label="To" value={address || ''} />
|
||||
<InfoItem label="Amount" value={`${amount?.amount} ${denom}`} />
|
||||
<InfoItem label="Transaction fee" value={`${fee?.nym || '-'} ${denom}`} />
|
||||
<InfoItem label="Total" value={`${calculateTotal()} ${denom}`} />
|
||||
</Stack>
|
||||
<Box display="flex" gap={2}>
|
||||
<Button variant="outlined" size="large" fullWidth onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="contained" size="large" fullWidth onClick={handleSend}>
|
||||
Send
|
||||
</Button>
|
||||
</Box>
|
||||
</PageLayout>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Divider, Stack, Typography } from '@mui/material';
|
||||
import { WalletAddressFormField } from '@nymproject/react/account/WalletAddressFormField';
|
||||
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
|
||||
import { DecCoin } from '@nymproject/types';
|
||||
import { Address, Button } from 'src/components';
|
||||
import { PageLayout } from 'src/layouts/PageLayout';
|
||||
import { SendProvider, useAppContext, useSendContext } from 'src/context';
|
||||
import { SendConfirmationPage } from './Confirmation';
|
||||
|
||||
const SendPage = ({ onConfirm }: { onConfirm: () => void }) => {
|
||||
const [isValidAddress, setIsValidAddress] = useState(false);
|
||||
const [isValidAmount, setIsValidAmount] = useState(false);
|
||||
|
||||
const { address, amount, handleChangeAddress, handleChangeAmount, handleGetFee } = useSendContext();
|
||||
const { balance } = useAppContext();
|
||||
|
||||
const handleNext = async () => {
|
||||
if (address && amount) {
|
||||
await handleGetFee(address, amount.amount);
|
||||
onConfirm();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<Stack gap={4} height="100%">
|
||||
<Address />
|
||||
<WalletAddressFormField
|
||||
showTickOnValid
|
||||
label="Recipient address"
|
||||
required
|
||||
onChanged={(_address: string) => handleChangeAddress(_address)}
|
||||
onValidate={setIsValidAddress}
|
||||
initialValue={address}
|
||||
/>
|
||||
<CurrencyFormField
|
||||
label="Amount"
|
||||
initialValue={amount?.amount}
|
||||
required
|
||||
onChanged={(_amount: DecCoin) => handleChangeAmount(_amount)}
|
||||
onValidate={(_: any, isValid: boolean) => setIsValidAmount(isValid)}
|
||||
/>
|
||||
<Box>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography fontWeight={600}>Account balance</Typography>
|
||||
<Typography fontWeight={600}>{balance} NYM</Typography>
|
||||
</Stack>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Typography variant="body2" sx={{ color: 'grey.600' }}>
|
||||
Est. fee for this transaction will be calculated on the next page
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
fullWidth
|
||||
disabled={!(isValidAddress && isValidAmount)}
|
||||
onClick={handleNext}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</PageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const Send = () => {
|
||||
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||
|
||||
return (
|
||||
<SendProvider>
|
||||
{showConfirmation ? (
|
||||
<SendConfirmationPage onCancel={() => setShowConfirmation(false)} />
|
||||
) : (
|
||||
<SendPage onConfirm={() => setShowConfirmation(true)} />
|
||||
)}
|
||||
</SendProvider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import React from 'react';
|
||||
|
||||
export const Settings = () => <h1>Settings</h1>;
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { Button } from 'src/components/ui';
|
||||
import { CenteredLogoLayout } from 'src/layouts';
|
||||
|
||||
export const SetupCompleteTemplate = ({
|
||||
title,
|
||||
description,
|
||||
onDone,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
onDone: () => void;
|
||||
}) => (
|
||||
<CenteredLogoLayout
|
||||
title={title}
|
||||
description={description}
|
||||
Actions={
|
||||
<Box width="100%">
|
||||
<Button variant="contained" fullWidth size="large" onClick={onDone}>
|
||||
Done
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import { FormControlLabel, Checkbox, Stack, Typography, Box } from '@mui/material';
|
||||
import { TopLogoLayout } from 'src/layouts/TopLogo';
|
||||
import { PasswordInput } from '@nymproject/react/textfields/Password';
|
||||
import { PasswordStrength } from '@nymproject/react/password-strength/PasswordStrength';
|
||||
import { Button } from 'src/components/ui';
|
||||
|
||||
type TCreatePassword = {
|
||||
canProceed: boolean;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
hasReadTerms: boolean;
|
||||
setHasReadTerms: (hasReadTerms: boolean) => void;
|
||||
setIsSafePassword: (isSafe: boolean) => void;
|
||||
setConfirmPassword: (password: string) => void;
|
||||
onNext: () => void;
|
||||
setPassword: (password: string) => void;
|
||||
};
|
||||
|
||||
export const CreatePasswordTemplate = ({
|
||||
canProceed,
|
||||
onNext,
|
||||
password,
|
||||
setPassword,
|
||||
confirmPassword,
|
||||
setIsSafePassword,
|
||||
setConfirmPassword,
|
||||
setHasReadTerms,
|
||||
hasReadTerms,
|
||||
}: TCreatePassword) => (
|
||||
<TopLogoLayout
|
||||
title="Create Password"
|
||||
description="Create a strong password - Min 8 characters, at least one capital letter, number and special character"
|
||||
Actions={
|
||||
<Button fullWidth variant="contained" size="large" disabled={!canProceed} onClick={onNext}>
|
||||
Next
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Stack spacing={1} mb={4}>
|
||||
<PasswordInput
|
||||
password={password}
|
||||
onUpdatePassword={(_password: string) => setPassword(_password)}
|
||||
label="Password"
|
||||
/>
|
||||
|
||||
<PasswordStrength password={password} handleIsSafePassword={(isSafe) => setIsSafePassword(isSafe)} />
|
||||
</Stack>
|
||||
|
||||
<Box mb={2}>
|
||||
<PasswordInput
|
||||
password={confirmPassword}
|
||||
onUpdatePassword={(_password: string) => setConfirmPassword(_password)}
|
||||
label="Confirm password"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<FormControlLabel
|
||||
label={<Typography variant="caption">I have read and agree with the Terms of use</Typography>}
|
||||
control={<Checkbox checked={hasReadTerms} onChange={(_, checked) => setHasReadTerms(checked)} />}
|
||||
/>
|
||||
</TopLogoLayout>
|
||||
);
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import { TextField } from '@mui/material';
|
||||
import { Button } from 'src/components';
|
||||
import { TopLogoLayout } from 'src/layouts';
|
||||
|
||||
export const ImportAccountTemplate = ({
|
||||
userMnemonic,
|
||||
onChangeUserMnemonic,
|
||||
onNext,
|
||||
}: {
|
||||
userMnemonic: string;
|
||||
onChangeUserMnemonic: (mnemonic: string) => void;
|
||||
onNext: () => void;
|
||||
}) => (
|
||||
<TopLogoLayout
|
||||
title="Import account"
|
||||
description="Type the mnemonic for the account you want to import "
|
||||
Actions={
|
||||
<Button variant="contained" fullWidth size="large" onClick={onNext}>
|
||||
Next
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<TextField
|
||||
label="Mnemonic"
|
||||
type="password"
|
||||
value={userMnemonic}
|
||||
onChange={(e) => onChangeUserMnemonic(e.target.value)}
|
||||
multiline
|
||||
autoFocus={false}
|
||||
fullWidth
|
||||
inputProps={{
|
||||
style: {
|
||||
height: '160px',
|
||||
},
|
||||
}}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
sx={{
|
||||
'input::-webkit-textfield-decoration-container': {
|
||||
alignItems: 'start',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</TopLogoLayout>
|
||||
);
|
||||
@@ -0,0 +1,60 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { Checkbox, FormControlLabel, Stack, TextField, Typography } from '@mui/material';
|
||||
import { TopLogoLayout } from 'src/layouts/TopLogo';
|
||||
import { Button } from 'src/components/ui';
|
||||
import { generateMnemonmic } from 'src/validator-client';
|
||||
|
||||
export const SeedPhraseTemplate = ({ onNext }: { onNext: (seedPhrase: string) => void }) => {
|
||||
const [isConfirmed, setIsconfirmed] = useState(false);
|
||||
|
||||
const seedPhrase = useRef(generateMnemonmic());
|
||||
|
||||
return (
|
||||
<TopLogoLayout
|
||||
title="Seed phrase"
|
||||
description="Save your seed phrase"
|
||||
Actions={
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
size="large"
|
||||
disabled={!isConfirmed}
|
||||
onClick={() => onNext(seedPhrase.current)}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Stack spacing={2} sx={{ mt: 2 }}>
|
||||
<Typography sx={{ textAlign: 'center', color: 'error.dark' }}>
|
||||
Below is your 24 word mnemonic, make sure to store it in a safe place for accessing your wallet in the future
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
label="Mnemonic"
|
||||
type="input"
|
||||
value={seedPhrase.current}
|
||||
multiline
|
||||
autoFocus={false}
|
||||
fullWidth
|
||||
inputProps={{
|
||||
style: {
|
||||
height: '160px',
|
||||
},
|
||||
}}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
sx={{
|
||||
'input::-webkit-textfield-decoration-container': {
|
||||
alignItems: 'start',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
label="I saved my mnemonic"
|
||||
control={<Checkbox checked={isConfirmed} onChange={(_, checked) => setIsconfirmed(checked)} />}
|
||||
/>
|
||||
</Stack>
|
||||
</TopLogoLayout>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './Complete';
|
||||
export * from './CreatePassword';
|
||||
export * from './ImportAccount';
|
||||
export * from './SeedPhrase';
|
||||
@@ -0,0 +1,35 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { BrowserRouter, MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||
import { Home } from 'src/pages';
|
||||
import { ExtensionStorage } from '@nymproject/extension-storage';
|
||||
import { RegisterRoutes } from './register';
|
||||
import { UserRoutes } from './user';
|
||||
import { LoginRoutes } from './login';
|
||||
|
||||
const Router = process.env.NODE_ENV === 'development' ? BrowserRouter : MemoryRouter;
|
||||
|
||||
export const AppRoutes = () => {
|
||||
const [userHasAccount, setUserHasAccount] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const checkUserHasAccount = async () => {
|
||||
const hasAccount = await ExtensionStorage.exists();
|
||||
setUserHasAccount(hasAccount);
|
||||
};
|
||||
|
||||
checkUserHasAccount();
|
||||
}, []);
|
||||
|
||||
if (userHasAccount === null) return null;
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={userHasAccount ? <LoginRoutes /> : <Home />} />
|
||||
<Route path="/login/*" element={<LoginRoutes />} />
|
||||
<Route path="/register/*" element={<RegisterRoutes />} />
|
||||
<Route path="/user/*" element={<UserRoutes />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Route, Routes, useNavigate } from 'react-router-dom';
|
||||
import { useAppContext } from 'src/context';
|
||||
import { ForgotPassword, Login } from 'src/pages/auth';
|
||||
|
||||
export const LoginRoutes = () => {
|
||||
const { client } = useAppContext();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
let route = '/login';
|
||||
|
||||
if (client) {
|
||||
route = '/user/balance';
|
||||
}
|
||||
|
||||
navigate(route);
|
||||
}, [client]);
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route index element={<Login />} />
|
||||
<Route path="/forgot-password" element={<ForgotPassword />} />
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { Route, Routes, useNavigate } from 'react-router-dom';
|
||||
import { RegisterContextProvider } from 'src/context/register';
|
||||
import { ImportAccount, SeedPhrase, SetupComplete } from 'src/pages/register';
|
||||
import { CreatePasswordOnExistingAccount } from 'src/pages/register/CreatePasswordOnExistingAccount';
|
||||
import { CreatePasswordOnNewAccount } from 'src/pages/register/CreatePasswordOnNewAccount';
|
||||
|
||||
export const RegisterRoutes = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSetUpComplete = () => {
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
return (
|
||||
<RegisterContextProvider>
|
||||
<Routes>
|
||||
<Route
|
||||
path="create-password"
|
||||
element={<CreatePasswordOnNewAccount onNext={() => navigate('/register/seed-phrase')} />}
|
||||
/>
|
||||
<Route path="seed-phrase" element={<SeedPhrase />} />
|
||||
<Route path="import-account" element={<ImportAccount />} />
|
||||
<Route
|
||||
path="import-account/create-password"
|
||||
element={
|
||||
<CreatePasswordOnExistingAccount
|
||||
onComplete={() => {
|
||||
navigate('/register/complete');
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route path="complete" element={<SetupComplete onDone={handleSetUpComplete} />} />
|
||||
</Routes>
|
||||
</RegisterContextProvider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import { RegisterContextProvider } from 'src/context/register';
|
||||
import { Accounts, AddAccount, ConfirmPassword, ImportAccount, NameAccount, SetupComplete } from 'src/pages';
|
||||
|
||||
export const AccountRoutes = () => (
|
||||
<RegisterContextProvider>
|
||||
<Routes>
|
||||
<Route path="/" element={<Accounts />} />
|
||||
<Route path="/add-account" element={<AddAccount />} />
|
||||
<Route path="/import-account" element={<ImportAccount />} />
|
||||
<Route path="/name-account" element={<NameAccount />} />
|
||||
<Route path="/confirm-password" element={<ConfirmPassword />} />
|
||||
<Route path="/complete" element={<SetupComplete />} />
|
||||
</Routes>
|
||||
</RegisterContextProvider>
|
||||
);
|
||||
@@ -0,0 +1,25 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Route, Routes, useNavigate } from 'react-router-dom';
|
||||
import { useAppContext } from 'src/context';
|
||||
import { Delegation, BalancePage, Receive, Send, Settings } from 'src/pages';
|
||||
import { AccountRoutes } from './accounts/accounts';
|
||||
|
||||
export const UserRoutes = () => {
|
||||
const { client } = useAppContext();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (!client) navigate('/login');
|
||||
}, [client]);
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/accounts/*" element={<AccountRoutes />} />
|
||||
<Route path="/balance" element={<BalancePage />} />
|
||||
<Route path="/delegation" element={<Delegation />} />
|
||||
<Route path="/receive" element={<Receive />} />
|
||||
<Route path="/send" element={<Send />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { CssBaseline, PaletteMode } from '@mui/material';
|
||||
import { createTheme, ThemeProvider } from '@mui/material/styles';
|
||||
import { getDesignTokens } from './theme';
|
||||
import '@assets/fonts/non-variable/fonts.css';
|
||||
|
||||
type TNymBrowserExtThemeProps = { mode: PaletteMode; children: React.ReactNode };
|
||||
|
||||
export const NymBrowserExtThemeWithMode = ({ mode, children }: TNymBrowserExtThemeProps) => {
|
||||
const theme = React.useMemo(() => createTheme(getDesignTokens(mode)), [mode]);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user