Compare commits

...

33 Commits

Author SHA1 Message Date
Mark Sinclair 6e1f8696da Bump nym-wallet version to 1.0.9 and update CHANGELOG 2022-09-08 16:08:07 +01:00
Mark Sinclair a00147f4b7 Add CHANGELOG from develop 2022-09-08 16:06:22 +01:00
Jon Häggblad 2f18666e5c update tests 2022-09-08 16:01:49 +01:00
Jon Häggblad fbcfcd5751 network-defaults: update mainnet default nymd_url 2022-09-08 16:01:33 +01:00
tommy d62638b8e2 update wallet version 2022-08-08 16:53:00 +02:00
Fouad 0dabff72bd Feature/bonding refactor (#1481)
* feat(wallet-bonding): bonding page, new bond form wip

* feat(wallet-bonding): add node table component

feat(wallet-bonding): new dialog component

* feat(wallet-bonding): node settings flow

* feat(wallet-bonding): bond more flow (done)

* feat(wallet): use confirmation modal component

* feat(wallet-bonding): node menu ui

* refactor(wallet-bonding): bonding flow with new gasFee estimation

* feat(wallet-bonding): unbond with gasFee and request

* refactor(wallet-bonding): switch to simpledialog component to keep modals consistency

* feat(wallet-bonding): fetch mixnode status

* update coin types in new bonding page

* fix displayed denom

* rebuild BondedNodeCard using existing shared components

* create reuseable ActionMenu component

* new mixnode form

* add gateway bond form

* check balance and fetch fee on bond mixnode request

* node settings

* get node description

* fix up rust request

* lint fixes + used NodeTypeSelector component

* temporarily remove estimated operator reward

* update return on rust function

* dont display node name UI if name doesnt exist

* rebase develop

* fix uppercase address bug

Co-authored-by: Mark Sinclair <mmsinclair@gmail.com>
Co-authored-by: pierre <dommerc.pierre@gmail.com>
2022-08-08 14:09:57 +01:00
Jędrzej Stuczyński fa354016e0 Removed useless into() conversion 2022-08-08 09:49:52 +01:00
Tommy Verrall 935ee765e9 Merge pull request #1498 from nymtech/feature/fix-envs
fix .env files for explorer.
2022-08-05 16:36:08 +01:00
tommy 4c8e59e6fc fix .env files for explorer. 2022-08-05 17:11:35 +02:00
Bogdan-Ștefan Neacşu 067f3e6f1a validator-api: handle SIGTERM (#1496)
* validator-api: handle SIGTERM

* Update CHANGELOG
2022-08-05 15:59:34 +03:00
Gala 6f09d46dce Merge pull request #1492 from nymtech/feature/delegating_ui_update
feat(wallet-delegation): new confirmation modal ui
2022-08-05 12:08:51 +02:00
Gala bdef48331b Merge pull request #1489 from nymtech/332-fix-vesting-ui
Wallet: Fixing dark mode colours in vesting shedule
2022-08-05 12:05:19 +02:00
Gala 51a6936e51 Merge pull request #1491 from nymtech/330-mix-wallet-ui
Wallet: Fix light mode bg and nav bar UI
2022-08-05 12:02:50 +02:00
Gala fd456d2952 Merge pull request #1490 from nymtech/334-ne-changes
NE: Filter by use routing score instead of stake parameter and adding filters info
2022-08-04 18:24:04 +02:00
Gala eee1abe593 removing extra styles 2022-08-04 18:15:12 +02:00
Gala fffad43937 Avoiding CAPS in button 2022-08-04 17:51:24 +02:00
Gala 3a79f43a8d styling 2022-08-04 17:23:32 +02:00
pierre 2e495f87ab refactor(wallet-delegation): ModalListItem component, pass colon in label value 2022-08-04 16:35:14 +02:00
Gala 57a9f18f5a fixing padding marging and wording 2022-08-04 16:30:33 +02:00
Gala 0c6a0a9cae Merge branch 'develop' into 334-ne-changes 2022-08-04 15:18:54 +02:00
Gala c80d8d354a adding nodes number info 2022-08-04 15:17:44 +02:00
Gala 3f544dbc69 adding Advanced filtering text and hover fix 2022-08-04 14:59:17 +02:00
pierre d1e1f15db0 Merge branch 'develop' into feature/delegating_ui_update 2022-08-04 13:26:41 +02:00
pierre 651c314182 feat(wallet-delegation): new confirmation modal ui 2022-08-04 13:20:00 +02:00
Gala a57545521d some refactor 2022-08-04 11:28:04 +02:00
Gala da60606921 hover bg color in dark and light mode 2022-08-03 17:20:46 +02:00
Gala 14f9bf7234 left nav spacing 2022-08-03 16:50:02 +02:00
Gala c1fa92869a fix light bg color 2022-08-03 16:49:47 +02:00
Gala c8533e3ec8 cleaning 2022-08-03 14:15:03 +02:00
Gala 06c4dd601d filter by use routing score instead of stake parameter 2022-08-03 14:06:33 +02:00
Gala 4ff80bbab2 fixing dark mode progress bar 2022-08-03 14:02:50 +02:00
Gala d7220b1fec adding info text in filters and renaming 2022-08-03 13:42:48 +02:00
Gala d92df9ada3 fixing dark mode colours 2022-08-03 12:24:21 +02:00
101 changed files with 3668 additions and 607 deletions
+2
View File
@@ -37,6 +37,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
- validator: fixed local docker-compose setup to work on Apple M1 ([#1329])
- explorer-api: listen out for SIGTERM and SIGQUIT too, making it play nicely as a system service ([#1482]).
- network-requester: fix filter for suffix-only domains ([#1487])
- validator-api: listen out for SIGTERM and SIGQUIT too, making it play nicely as a system service ([#1496]).
### Changed
@@ -77,6 +78,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
[#1478]: https://github.com/nymtech/nym/pull/1478
[#1482]: https://github.com/nymtech/nym/pull/1482
[#1487]: https://github.com/nymtech/nym/pull/1487
[#1496]: https://github.com/nymtech/nym/pull/1496
## [nym-connect-v1.0.1](https://github.com/nymtech/nym/tree/nym-connect-v1.0.1) (2022-07-22)
Generated
+1
View File
@@ -3325,6 +3325,7 @@ dependencies = [
"serde",
"serde_json",
"sqlx",
"task",
"thiserror",
"time 0.3.9",
"tokio",
@@ -4,8 +4,8 @@ import expect from 'expect';
describe('Query: balances', () => {
it('can query for an account balance', async () => {
const client = await ValidatorClient.connectForQuery(
'https://rpc.nyx.nodes.guru/', 'https://validator.nymtech.net/api/', 'n', 'n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g', 'n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw', 'nym');
'https://rpc.nymtech.net/', 'https://validator.nymtech.net/api/', 'n', 'n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g', 'n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw', 'nym');
const balance = await client.getBalance('n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy');
expect(Number.parseFloat(balance.amount)).toBeGreaterThan(0);
}).timeout(5000);
})
})
+2 -2
View File
@@ -24,8 +24,8 @@ pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] =
hex_literal::hex!("0000000000000000000000000000000000000000");
pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy";
pub(crate) const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "http://127.0.0.1:8090";
pub const NYMD_VALIDATOR: &str = "https://rpc.nyx.nodes.guru/";
pub(crate) const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "https://mainnet-stats.nymte.ch:8090/";
pub const NYMD_VALIDATOR: &str = "https://rpc.nymtech.net";
pub const API_VALIDATOR: &str = "https://validator.nymtech.net/api/";
pub(crate) fn validators() -> Vec<ValidatorDetails> {
vec![ValidatorDetails::new(NYMD_VALIDATOR, Some(API_VALIDATOR))]
+1 -1
View File
@@ -507,7 +507,7 @@ mod test {
for (expected, raw_display) in values {
let coin = DecCoin {
denom: Network::MAINNET.mix_denom().display.into(),
denom: Network::MAINNET.mix_denom().display,
amount: raw_display.parse().unwrap(),
};
let base = reg.attempt_convert_to_base_coin(coin).unwrap();
+1
View File
@@ -1,5 +1,6 @@
EXPLORER_API_URL=https://explorer.nymtech.net/api/v1
VALIDATOR_API_URL=https://validator.nymtech.net
VALIDATOR_URL=https://rpc.nyx.nodes.guru
BIG_DIPPER_URL=https://blocks.nymtech.net
CURRENCY_DENOM=unym
CURRENCY_STAKING_DENOM=unyx
+2 -1
View File
@@ -1,5 +1,6 @@
EXPLORER_API_URL=https://qa-explorer.nymtech.net/api/v1
VALIDATOR_API_URL=https://qa-validator.nymtech.net
VALIDATOR_API_URL=https://qa-validator-api.nymtech.net
VALIDATOR_URL=https://qa-validator.nymtech.net
BIG_DIPPER_URL=https://qa-blocks.nymtech.net
CURRENCY_DENOM=unymt
CURRENCY_STAKING_DENOM=unyxt
+2 -1
View File
@@ -1,6 +1,7 @@
// master APIs
export const API_BASE_URL = process.env.EXPLORER_API_URL;
export const VALIDATOR_API_BASE_URL = process.env.VALIDATOR_API_URL;
export const VALIDATOR_URL = process.env.VALIDATOR_URL;
export const BIG_DIPPER = process.env.BIG_DIPPER_URL;
// specific API routes
@@ -9,7 +10,7 @@ export const MIXNODE_PING = `${API_BASE_URL}/ping`;
export const MIXNODES_API = `${API_BASE_URL}/mix-nodes`;
export const MIXNODE_API = `${API_BASE_URL}/mix-node`;
export const GATEWAYS_API = `${VALIDATOR_API_BASE_URL}/api/v1/gateways`;
export const VALIDATORS_API = `${VALIDATOR_API_BASE_URL}/validators`;
export const VALIDATORS_API = `${VALIDATOR_URL}/validators`;
export const BLOCK_API = `${VALIDATOR_API_BASE_URL}/block`;
export const COUNTRY_DATA_API = `${API_BASE_URL}/countries`;
export const UPTIME_STORY_API = `${VALIDATOR_API_BASE_URL}/api/v1/status/mixnode`; // add ID then '/history' to this.
+15 -8
View File
@@ -6,7 +6,6 @@ import {
DialogContent,
DialogActions,
DialogTitle,
IconButton,
Slider,
Typography,
Box,
@@ -25,6 +24,7 @@ import { useIsMobile } from '../../hooks/useIsMobile';
const FilterItem = ({
label,
id,
tooltipInfo,
value,
marks,
scale,
@@ -36,6 +36,7 @@ const FilterItem = ({
}) => (
<Box sx={{ p: 2 }}>
<Typography gutterBottom>{label}</Typography>
<Typography fontSize={12}>{tooltipInfo}</Typography>
<Slider
value={value}
onChange={(e: Event, newValue: number | number[]) => onChange(id, newValue as number[])}
@@ -50,7 +51,7 @@ const FilterItem = ({
);
export const Filters = () => {
const { filterMixnodes, fetchMixnodes } = useMainContext();
const { filterMixnodes, fetchMixnodes, mixnodes } = useMainContext();
const { status } = useParams<{ status: MixnodeStatusWithAll | undefined }>();
const isMobile = useIsMobile();
@@ -129,17 +130,23 @@ export const Filters = () => {
variant={isMobile ? 'standard' : 'outlined'}
action={
<Button size="small" onClick={onClearFilters}>
Clear
CLEAR FILTERS
</Button>
}
sx={{ width: 300 }}
>
Filters applied
{mixnodes?.data?.length} mixnodes matched your criteria
</Alert>
</Snackbar>
<IconButton size="large" onClick={handleToggleShowFilters}>
<Tune />
</IconButton>
<Button
size="large"
variant="text"
color="inherit"
endIcon={<Tune />}
onClick={handleToggleShowFilters}
sx={{ textTransform: 'none' }}
>
Advanced filters
</Button>
<Dialog open={showFilters} onClose={handleToggleShowFilters} maxWidth="md" fullWidth>
<DialogTitle>Mixnode filters</DialogTitle>
<DialogContent dividers>
+20 -54
View File
@@ -18,6 +18,8 @@ export const generateFilterSchema = (upperSaturationValue?: number) => ({
{ label: '90', value: 90 },
{ label: '100', value: 100 },
],
tooltipInfo:
'As a delegator you want to chose nodes with lower profit margin, meaning more payout for their delegators',
},
stakeSaturation: {
label: 'Stake saturation (%)',
@@ -43,65 +45,29 @@ export const generateFilterSchema = (upperSaturationValue?: number) => ({
},
],
max: upperSaturationValue,
tooltipInfo: "Select nodes with <100% saturation. Any additional stake above 100% saturation won't get rewards",
},
stake: {
label: 'Stake (NYM)',
id: EnumFilterKey.stake,
value: [20, 90],
min: 20,
max: 90,
routingScore: {
label: 'Routing score (%)',
id: EnumFilterKey.routingScore,
value: [0, 100],
marks: [
{
value: 0,
label: '1',
},
{
value: 10,
label: '10',
},
{
value: 20,
label: '100',
},
{
value: 30,
label: '1k',
},
{
value: 40,
label: '10k',
},
{
value: 50,
label: '100k',
},
{
value: 60,
label: '1M',
},
{
value: 70,
label: '10M',
},
{
value: 80,
label: '100M',
},
{
value: 90,
label: '1B',
},
{ label: '0', value: 0 },
{ label: '10', value: 10 },
{ label: '20', value: 20 },
{ label: '30', value: 30 },
{ label: '40', value: 40 },
{ label: '50', value: 50 },
{ label: '60', value: 60 },
{ label: '70', value: 70 },
{ label: '80', value: 80 },
{ label: '90', value: 90 },
{ label: '100', value: 100 },
],
tooltipInfo: 'The higher the routing score the better the performance of the node and so its rewards',
},
});
const formatStakeValuesToMinorDenom = ([value_1, value_2]: number[]) => {
const lowerValue = 10 ** (value_1 / 10) * 1_000_000;
const upperValue = 10 ** (value_2 / 10) * 1_000_000;
return [lowerValue, upperValue];
};
const formatStakeSaturationValues = ([value_1, value_2]: number[]) => {
const lowerValue = value_1 / 100;
const upperValue = value_2 / 100;
@@ -110,7 +76,7 @@ const formatStakeSaturationValues = ([value_1, value_2]: number[]) => {
};
export const formatOnSave = (filters: TFilters) => ({
stake: formatStakeValuesToMinorDenom(filters.stake.value),
routingScore: filters.routingScore.value,
profitMargin: filters.profitMargin.value,
stakeSaturation: formatStakeSaturationValues(filters.stakeSaturation.value),
});
+2 -2
View File
@@ -102,8 +102,8 @@ export const MainContextProvider: React.FC = ({ children }) => {
m.mix_node.profit_margin_percent <= filters.profitMargin[1] &&
m.stake_saturation >= filters.stakeSaturation[0] &&
m.stake_saturation <= filters.stakeSaturation[1] &&
+m.pledge_amount.amount + +m.total_delegation.amount >= filters.stake[0] &&
+m.pledge_amount.amount + +m.total_delegation.amount <= filters.stake[1],
m.avg_uptime >= filters.routingScore[0] &&
m.avg_uptime <= filters.routingScore[1],
);
setMixnodes({ data: filtered, isLoading: false });
};
+2 -1
View File
@@ -4,7 +4,7 @@ import { Mark } from '@mui/base';
export enum EnumFilterKey {
profitMargin = 'profitMargin',
stakeSaturation = 'stakeSaturation',
stake = 'stake',
routingScore = 'routingScore',
}
export type TFilterItem = {
@@ -15,6 +15,7 @@ export type TFilterItem = {
min?: number;
max?: number;
scale?: (value: number) => number;
tooltipInfo?: string;
};
export type TFilters = { [key in EnumFilterKey]: TFilterItem };
+217
View File
@@ -0,0 +1,217 @@
## [nym-wallet-v1.0.9](https://github.com/nymtech/nym/releases/tag/nym-wallet-v1.0.8) (2022-09-08)
- wallet: change default `nymd` URL to https://rpc.nymtech.net
## [nym-wallet-v1.0.8](https://github.com/nymtech/nym/releases/tag/nym-wallet-v1.0.8) (2022-08-11)
- wallet: new bonding flow and screen for bonded node
- wallet: compound and redeem functionalities for operator rewards
- wallet: a few minor touch ups and bug fixes
## [nym-wallet-v1.0.7](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.7) (2022-07-11)
- wallet: dark mode
- wallet: when simulating gas costs, an automatic adjustment is being used ([#1388]).
[#1388]: https://github.com/nymtech/nym/pull/1388
## [nym-wallet-v1.0.6](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.6) (2022-06-21)
- wallet: undelegating now uses either the mixnet or vesting contract, or both, depending on how delegations were made
- wallet: redeeming and compounding now uses both the mixnet and vesting contract
- wallet: the wallet backend learned how to archive wallet files
- wallet: add ENABLE_QA_MODE environment variable to enable QA mode on built wallet
## [nym-wallet-v1.0.5](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.5) (2022-06-14)
- wallet: add simple CLI tool for decrypting and recovering the wallet file.
- wallet: added support for multiple accounts ([#1265])
- wallet: compound and claim reward endpoints for operators and delegators ([#1302])
- wallet: require password to switch accounts
- wallet: the wallet backend learned how to keep track of validator name, either hardcoded or by querying the status endpoint.
- wallet: new delegation and rewards UI
- wallet: show version in nav bar
- wallet: contract admin route put back
- wallet: staking_supply field to StateParams
- wallet: show transaction hash for redeeming or compounding rewards
[#1265]: https://github.com/nymtech/nym/pull/1265
[#1302]: https://github.com/nymtech/nym/pull/1302
## [nym-wallet-v1.0.4](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.4) (2022-05-04)
### Changed
- all: the default behaviour of validator client is changed to use `broadcast_sync` and poll for transaction inclusion instead of using `broadcast_commit` to deal with timeouts ([#1246])
## [nym-wallet-v1.0.3](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.3) (2022-04-25)
[Full Changelog](https://github.com/nymtech/nym/compare/nym-binaries-1.0.0-rc.2...nym-wallet-v1.0.3)
**Fixed bugs:**
- \[Issue\] Wallet 1.0.2 cannot send NYM tokens from a DelayedVestingAccount [\#1215](https://github.com/nymtech/nym/issues/1215)
- Main README not showing properly with GitHub dark mode [\#1211](https://github.com/nymtech/nym/issues/1211)
**Merged pull requests:**
- Bugfix - wallet undelegation for vesting accounts [\#1220](https://github.com/nymtech/nym/pull/1220) ([mmsinclair](https://github.com/mmsinclair))
- Bugfix/delegation reconcile [\#1219](https://github.com/nymtech/nym/pull/1219) ([jstuczyn](https://github.com/jstuczyn))
- Bugfix/query proxied pending delegations [\#1218](https://github.com/nymtech/nym/pull/1218) ([jstuczyn](https://github.com/jstuczyn))
- Using custom gas multiplier in the wallet [\#1217](https://github.com/nymtech/nym/pull/1217) ([jstuczyn](https://github.com/jstuczyn))
- Feature/vesting accounts support [\#1216](https://github.com/nymtech/nym/pull/1216) ([jstuczyn](https://github.com/jstuczyn))
- Release/1.0.0 rc.2 [\#1214](https://github.com/nymtech/nym/pull/1214) ([jstuczyn](https://github.com/jstuczyn))
- chore: fix dark mode rendering [\#1212](https://github.com/nymtech/nym/pull/1212) ([pwnfoo](https://github.com/pwnfoo))
- Feature/spend coconut [\#1210](https://github.com/nymtech/nym/pull/1210) ([neacsu](https://github.com/neacsu))
- Bugfix/unique sphinx key [\#1207](https://github.com/nymtech/nym/pull/1207) ([jstuczyn](https://github.com/jstuczyn))
- Add cache read and write timeouts [\#1206](https://github.com/nymtech/nym/pull/1206) ([durch](https://github.com/durch))
- Additional, more informative routes [\#1204](https://github.com/nymtech/nym/pull/1204) ([durch](https://github.com/durch))
- Feature/aggregated econ dynamics explorer endpoint [\#1203](https://github.com/nymtech/nym/pull/1203) ([jstuczyn](https://github.com/jstuczyn))
- Debugging validator [\#1198](https://github.com/nymtech/nym/pull/1198) ([durch](https://github.com/durch))
- wallet: expose additional validator configuration functionality to the frontend [\#1195](https://github.com/nymtech/nym/pull/1195) ([octol](https://github.com/octol))
- Update rewarding validator address [\#1193](https://github.com/nymtech/nym/pull/1193) ([durch](https://github.com/durch))
- Crypto part of the Groth's NIDKG [\#1182](https://github.com/nymtech/nym/pull/1182) ([jstuczyn](https://github.com/jstuczyn))
- fix unbond page [\#1180](https://github.com/nymtech/nym/pull/1180) ([tommyv1987](https://github.com/tommyv1987))
- Type safe bounds [\#1179](https://github.com/nymtech/nym/pull/1179) ([durch](https://github.com/durch))
- Fix delegation paging [\#1174](https://github.com/nymtech/nym/pull/1174) ([durch](https://github.com/durch))
- Update binaries to rc version [\#1172](https://github.com/nymtech/nym/pull/1172) ([tommyv1987](https://github.com/tommyv1987))
- Bump ansi-regex from 4.1.0 to 4.1.1 in /docker/typescript\_client/upload\_contract [\#1171](https://github.com/nymtech/nym/pull/1171) ([dependabot[bot]](https://github.com/apps/dependabot))
## [nym-wallet-v1.0.2](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.2) (2022-04-05)
[Full Changelog](https://github.com/nymtech/nym/compare/nym-wallet-v1.0.1...nym-wallet-v1.0.2)
**Merged pull requests:**
- Wallet 1.0.2 visual tweaks [\#1197](https://github.com/nymtech/nym/pull/1197) ([mmsinclair](https://github.com/mmsinclair))
- Password for wallet with routes [\#1196](https://github.com/nymtech/nym/pull/1196) ([fmtabbara](https://github.com/fmtabbara))
- Add auto-updater to Nym Wallet [\#1194](https://github.com/nymtech/nym/pull/1194) ([mmsinclair](https://github.com/mmsinclair))
- Fix clippy warnings for beta toolchain [\#1191](https://github.com/nymtech/nym/pull/1191) ([octol](https://github.com/octol))
- wallet: expose validator urls to the frontend [\#1190](https://github.com/nymtech/nym/pull/1190) ([octol](https://github.com/octol))
- wallet: add test for decrypting stored wallet file [\#1189](https://github.com/nymtech/nym/pull/1189) ([octol](https://github.com/octol))
- Fix clippy warnings [\#1188](https://github.com/nymtech/nym/pull/1188) ([octol](https://github.com/octol))
- Password for wallet with routes [\#1187](https://github.com/nymtech/nym/pull/1187) ([mmsinclair](https://github.com/mmsinclair))
- wallet: add validate\_mnemonic [\#1186](https://github.com/nymtech/nym/pull/1186) ([octol](https://github.com/octol))
- wallet: support removing accounts from the wallet file [\#1185](https://github.com/nymtech/nym/pull/1185) ([octol](https://github.com/octol))
- Feature/adding discord [\#1184](https://github.com/nymtech/nym/pull/1184) ([gala1234](https://github.com/gala1234))
- wallet: config backend for validator selection [\#1183](https://github.com/nymtech/nym/pull/1183) ([octol](https://github.com/octol))
- Add storybook to wallet [\#1178](https://github.com/nymtech/nym/pull/1178) ([mmsinclair](https://github.com/mmsinclair))
- wallet: connection test nymd and api urls independently [\#1170](https://github.com/nymtech/nym/pull/1170) ([octol](https://github.com/octol))
- wallet: wire up account storage [\#1153](https://github.com/nymtech/nym/pull/1153) ([octol](https://github.com/octol))
- Feature/signature on deposit [\#1151](https://github.com/nymtech/nym/pull/1151) ([neacsu](https://github.com/neacsu))
## [nym-wallet-v1.0.1](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.1) (2022-04-05)
[Full Changelog](https://github.com/nymtech/nym/compare/nym-binaries-1.0.0-rc.1...nym-wallet-v1.0.1)
**Closed issues:**
- Check enabling bbbc simultaneously with open access. Estimate what it would take to make this the default compilation target. [\#1175](https://github.com/nymtech/nym/issues/1175)
- Get coconut credential for deposited tokens [\#1138](https://github.com/nymtech/nym/issues/1138)
- Make payments lazy [\#1135](https://github.com/nymtech/nym/issues/1135)
- Uptime on node selection for sets [\#1049](https://github.com/nymtech/nym/issues/1049)
## [nym-wallet-v1.0.0](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.0) (2022-02-03)
[Full Changelog](https://github.com/nymtech/nym/compare/v0.12.1...nym-wallet-v1.0.0)
**Implemented enhancements:**
- \[Feature Request\] Please enable registration without need for Telegram account [\#1016](https://github.com/nymtech/nym/issues/1016)
- Fast mixnode launch with a pre-built ISO + VM software [\#1001](https://github.com/nymtech/nym/issues/1001)
**Fixed bugs:**
- \[Issue\] [\#1000](https://github.com/nymtech/nym/issues/1000)
- \[Issue\] `nym-client` requires multiple attempts to run a server [\#869](https://github.com/nymtech/nym/issues/869)
- De-'float'-ing `Interval` \(`Display` impl + `serde`\) [\#1065](https://github.com/nymtech/nym/pull/1065) ([jstuczyn](https://github.com/jstuczyn))
- display client address on wallet creation [\#1058](https://github.com/nymtech/nym/pull/1058) ([fmtabbara](https://github.com/fmtabbara))
**Closed issues:**
- Rewarded set inclusion probability API endpoint [\#1037](https://github.com/nymtech/nym/issues/1037)
- Update cw-storage-plus to 0.11 [\#1032](https://github.com/nymtech/nym/issues/1032)
- Change `u128` fields in `RewardEstimationResponse` to `u64` [\#1029](https://github.com/nymtech/nym/issues/1029)
- Test out the mainnet Gravity Bridge [\#1006](https://github.com/nymtech/nym/issues/1006)
- Add vesting contract interface to nym-wallet [\#959](https://github.com/nymtech/nym/issues/959)
- Mixnode crash [\#486](https://github.com/nymtech/nym/issues/486)
**Merged pull requests:**
- create custom urls for mainnet [\#1095](https://github.com/nymtech/nym/pull/1095) ([fmtabbara](https://github.com/fmtabbara))
- Wallet signing on MacOS [\#1093](https://github.com/nymtech/nym/pull/1093) ([mmsinclair](https://github.com/mmsinclair))
- Fix rust 2018 idioms warnings [\#1092](https://github.com/nymtech/nym/pull/1092) ([octol](https://github.com/octol))
- Prevent contract overwriting [\#1090](https://github.com/nymtech/nym/pull/1090) ([durch](https://github.com/durch))
- Logout operation [\#1087](https://github.com/nymtech/nym/pull/1087) ([jstuczyn](https://github.com/jstuczyn))
- Update to rust edition 2021 everywhere [\#1086](https://github.com/nymtech/nym/pull/1086) ([octol](https://github.com/octol))
- Tag contract errors, and print out lines for easier QA [\#1084](https://github.com/nymtech/nym/pull/1084) ([durch](https://github.com/durch))
- Feature/flexible vesting + utility queries [\#1083](https://github.com/nymtech/nym/pull/1083) ([durch](https://github.com/durch))
- Bump @openzeppelin/contracts from 4.3.1 to 4.4.2 in /contracts/basic-bandwidth-generation [\#1082](https://github.com/nymtech/nym/pull/1082) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump nth-check from 2.0.0 to 2.0.1 in /clients/native/examples/js-examples/websocket [\#1081](https://github.com/nymtech/nym/pull/1081) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump url-parse from 1.5.1 to 1.5.4 in /clients/native/examples/js-examples/websocket [\#1080](https://github.com/nymtech/nym/pull/1080) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump follow-redirects from 1.14.1 to 1.14.7 in /clients/native/examples/js-examples/websocket [\#1079](https://github.com/nymtech/nym/pull/1079) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump nanoid from 3.1.23 to 3.2.0 in /clients/native/examples/js-examples/websocket [\#1078](https://github.com/nymtech/nym/pull/1078) ([dependabot[bot]](https://github.com/apps/dependabot))
- Setup basic test for mixnode stats reporting [\#1077](https://github.com/nymtech/nym/pull/1077) ([octol](https://github.com/octol))
- Make wallet\_address mandatory for mixnode init [\#1076](https://github.com/nymtech/nym/pull/1076) ([octol](https://github.com/octol))
- Tidy nym-mixnode module visibility [\#1075](https://github.com/nymtech/nym/pull/1075) ([octol](https://github.com/octol))
- Feature/wallet login with password [\#1074](https://github.com/nymtech/nym/pull/1074) ([fmtabbara](https://github.com/fmtabbara))
- Add trait to mock client dependency in DelayForwarder [\#1073](https://github.com/nymtech/nym/pull/1073) ([octol](https://github.com/octol))
- Bump rust-version to latest stable for nym-mixnode [\#1072](https://github.com/nymtech/nym/pull/1072) ([octol](https://github.com/octol))
- Fixes CI for our wasm build [\#1069](https://github.com/nymtech/nym/pull/1069) ([jstuczyn](https://github.com/jstuczyn))
- Add @octol as codeowner [\#1068](https://github.com/nymtech/nym/pull/1068) ([octol](https://github.com/octol))
- set-up inclusion probability [\#1067](https://github.com/nymtech/nym/pull/1067) ([fmtabbara](https://github.com/fmtabbara))
- Feature/wasm client [\#1066](https://github.com/nymtech/nym/pull/1066) ([neacsu](https://github.com/neacsu))
- Changed bech32\_prefix from punk to nymt [\#1064](https://github.com/nymtech/nym/pull/1064) ([jstuczyn](https://github.com/jstuczyn))
- Bump nanoid from 3.1.30 to 3.2.0 in /testnet-faucet [\#1063](https://github.com/nymtech/nym/pull/1063) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump nanoid from 3.1.30 to 3.2.0 in /nym-wallet [\#1062](https://github.com/nymtech/nym/pull/1062) ([dependabot[bot]](https://github.com/apps/dependabot))
- Rework vesting contract storage [\#1061](https://github.com/nymtech/nym/pull/1061) ([durch](https://github.com/durch))
- Mixnet Contract constants extraction [\#1060](https://github.com/nymtech/nym/pull/1060) ([jstuczyn](https://github.com/jstuczyn))
- fix: make explorer footer year dynamic [\#1059](https://github.com/nymtech/nym/pull/1059) ([martinyung](https://github.com/martinyung))
- Add mnemonic just on creation, to display it [\#1057](https://github.com/nymtech/nym/pull/1057) ([neacsu](https://github.com/neacsu))
- Network Explorer: updates to API and UI to show the active set [\#1056](https://github.com/nymtech/nym/pull/1056) ([mmsinclair](https://github.com/mmsinclair))
- Made contract addresses for query NymdClient construction optional [\#1055](https://github.com/nymtech/nym/pull/1055) ([jstuczyn](https://github.com/jstuczyn))
- Introduced RPC query for total token supply [\#1053](https://github.com/nymtech/nym/pull/1053) ([jstuczyn](https://github.com/jstuczyn))
- Feature/tokio console [\#1052](https://github.com/nymtech/nym/pull/1052) ([durch](https://github.com/durch))
- Implemented beta clippy lint recommendations [\#1051](https://github.com/nymtech/nym/pull/1051) ([jstuczyn](https://github.com/jstuczyn))
- add new function to update profit percentage [\#1050](https://github.com/nymtech/nym/pull/1050) ([fmtabbara](https://github.com/fmtabbara))
- Upgrade Clap and use declarative argument parsing for nym-mixnode [\#1047](https://github.com/nymtech/nym/pull/1047) ([octol](https://github.com/octol))
- Feature/additional bond validation [\#1046](https://github.com/nymtech/nym/pull/1046) ([fmtabbara](https://github.com/fmtabbara))
- Fix clippy on relevant lints [\#1044](https://github.com/nymtech/nym/pull/1044) ([neacsu](https://github.com/neacsu))
- Bump shelljs from 0.8.4 to 0.8.5 in /contracts/basic-bandwidth-generation [\#1043](https://github.com/nymtech/nym/pull/1043) ([dependabot[bot]](https://github.com/apps/dependabot))
- Endpoint for rewarded set inclusion probabilities [\#1042](https://github.com/nymtech/nym/pull/1042) ([durch](https://github.com/durch))
- Bump follow-redirects from 1.14.4 to 1.14.7 in /contracts/basic-bandwidth-generation [\#1041](https://github.com/nymtech/nym/pull/1041) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump follow-redirects from 1.14.5 to 1.14.7 in /testnet-faucet [\#1040](https://github.com/nymtech/nym/pull/1040) ([dependabot[bot]](https://github.com/apps/dependabot))
- Feature/node settings update [\#1036](https://github.com/nymtech/nym/pull/1036) ([fmtabbara](https://github.com/fmtabbara))
- Migrate to cw-storage-plus 0.11.1 [\#1035](https://github.com/nymtech/nym/pull/1035) ([durch](https://github.com/durch))
- Bump @openzeppelin/contracts from 4.4.1 to 4.4.2 in /contracts/basic-bandwidth-generation [\#1034](https://github.com/nymtech/nym/pull/1034) ([dependabot[bot]](https://github.com/apps/dependabot))
- Feature/configurable wallet [\#1033](https://github.com/nymtech/nym/pull/1033) ([neacsu](https://github.com/neacsu))
- Feature/downcast reward estimation [\#1031](https://github.com/nymtech/nym/pull/1031) ([durch](https://github.com/durch))
- Wallet UI updates [\#1028](https://github.com/nymtech/nym/pull/1028) ([fmtabbara](https://github.com/fmtabbara))
- Remove migration code [\#1027](https://github.com/nymtech/nym/pull/1027) ([neacsu](https://github.com/neacsu))
- Chore/stricter dependency requirements [\#1025](https://github.com/nymtech/nym/pull/1025) ([jstuczyn](https://github.com/jstuczyn))
- Feature/validator api client endpoints [\#1024](https://github.com/nymtech/nym/pull/1024) ([jstuczyn](https://github.com/jstuczyn))
- Updated cosmrs to 0.4.1 [\#1023](https://github.com/nymtech/nym/pull/1023) ([jstuczyn](https://github.com/jstuczyn))
- Feature/testnet deploy scripts [\#1022](https://github.com/nymtech/nym/pull/1022) ([mfahampshire](https://github.com/mfahampshire))
- Changed wallet's client to a full validator client [\#1021](https://github.com/nymtech/nym/pull/1021) ([jstuczyn](https://github.com/jstuczyn))
- Fix 404 link [\#1020](https://github.com/nymtech/nym/pull/1020) ([RiccardoMasutti](https://github.com/RiccardoMasutti))
- Feature/additional mixnode endpoints [\#1019](https://github.com/nymtech/nym/pull/1019) ([jstuczyn](https://github.com/jstuczyn))
- Introduced denom check when trying to withdraw vested coins [\#1018](https://github.com/nymtech/nym/pull/1018) ([jstuczyn](https://github.com/jstuczyn))
- Add network defaults for qa [\#1017](https://github.com/nymtech/nym/pull/1017) ([neacsu](https://github.com/neacsu))
- Feature/expanded events [\#1015](https://github.com/nymtech/nym/pull/1015) ([jstuczyn](https://github.com/jstuczyn))
- update frontend to use new profit update api [\#1014](https://github.com/nymtech/nym/pull/1014) ([fmtabbara](https://github.com/fmtabbara))
- Feature/node state endpoint [\#1013](https://github.com/nymtech/nym/pull/1013) ([jstuczyn](https://github.com/jstuczyn))
- Feature/hourly set updates [\#1012](https://github.com/nymtech/nym/pull/1012) ([durch](https://github.com/durch))
- Feature/remove unused profit margin [\#1011](https://github.com/nymtech/nym/pull/1011) ([neacsu](https://github.com/neacsu))
- Feature/explorer node status [\#1010](https://github.com/nymtech/nym/pull/1010) ([jstuczyn](https://github.com/jstuczyn))
- Use serial integer instead of random [\#1009](https://github.com/nymtech/nym/pull/1009) ([durch](https://github.com/durch))
- Feature/configure profit [\#1008](https://github.com/nymtech/nym/pull/1008) ([neacsu](https://github.com/neacsu))
- Feature/fix gateway sign [\#1004](https://github.com/nymtech/nym/pull/1004) ([neacsu](https://github.com/neacsu))
- Fix clippy [\#1003](https://github.com/nymtech/nym/pull/1003) ([neacsu](https://github.com/neacsu))
- Update wallet version [\#998](https://github.com/nymtech/nym/pull/998) ([tommyv1987](https://github.com/tommyv1987))
- Fix wallet build instructions [\#997](https://github.com/nymtech/nym/pull/997) ([tommyv1987](https://github.com/tommyv1987))
- Make the separation between testnet-mode and erc20 bandwidth mode clearer [\#994](https://github.com/nymtech/nym/pull/994) ([neacsu](https://github.com/neacsu))
- Bump @openzeppelin/contracts from 3.4.0 to 4.4.1 in /contracts/basic-bandwidth-generation [\#983](https://github.com/nymtech/nym/pull/983) ([dependabot[bot]](https://github.com/apps/dependabot))
- Feature/implicit runtime [\#973](https://github.com/nymtech/nym/pull/973) ([jstuczyn](https://github.com/jstuczyn))
- Differentiate staking and ownership [\#961](https://github.com/nymtech/nym/pull/961) ([durch](https://github.com/durch))
+88 -11
View File
@@ -3055,7 +3055,7 @@ dependencies = [
[[package]]
name = "nym_wallet"
version = "1.0.7"
version = "1.0.9"
dependencies = [
"aes-gcm",
"argon2 0.3.4",
@@ -3273,7 +3273,17 @@ checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [
"instant",
"lock_api",
"parking_lot_core",
"parking_lot_core 0.8.5",
]
[[package]]
name = "parking_lot"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
dependencies = [
"lock_api",
"parking_lot_core 0.9.3",
]
[[package]]
@@ -3290,6 +3300,19 @@ dependencies = [
"winapi",
]
[[package]]
name = "parking_lot_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec 1.8.0",
"windows-sys",
]
[[package]]
name = "password-hash"
version = "0.3.2"
@@ -4550,6 +4573,15 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
dependencies = [
"libc",
]
[[package]]
name = "signature"
version = "1.3.2"
@@ -4635,9 +4667,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "state"
version = "0.5.2"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87cf4f5369e6d3044b5e365c9690f451516ac8f0954084622b49ea3fde2f6de5"
checksum = "dbe866e1e51e8260c9eed836a042a5e7f6726bb2b411dffeaa712e19c388f23b"
dependencies = [
"loom",
]
@@ -4656,7 +4688,7 @@ checksum = "33994d0838dc2d152d17a62adf608a869b5e846b65b389af7f3dbc1de45c5b26"
dependencies = [
"lazy_static",
"new_debug_unreachable",
"parking_lot",
"parking_lot 0.11.2",
"phf_shared 0.10.0",
"precomputed-hash",
"serde",
@@ -4838,7 +4870,7 @@ dependencies = [
"ndk-glue",
"ndk-sys",
"objc",
"parking_lot",
"parking_lot 0.11.2",
"raw-window-handle",
"scopeguard",
"serde",
@@ -5241,7 +5273,9 @@ dependencies = [
"mio",
"num_cpus",
"once_cell",
"parking_lot 0.12.1",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"winapi",
@@ -5898,11 +5932,11 @@ version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b749ebd2304aa012c5992d11a25d07b406bdbe5f79d371cb7a918ce501a19eb0"
dependencies = [
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_msvc",
"windows_aarch64_msvc 0.30.0",
"windows_i686_gnu 0.30.0",
"windows_i686_msvc 0.30.0",
"windows_x86_64_gnu 0.30.0",
"windows_x86_64_msvc 0.30.0",
]
[[package]]
@@ -5915,12 +5949,31 @@ dependencies = [
"windows_reader",
]
[[package]]
name = "windows-sys"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
dependencies = [
"windows_aarch64_msvc 0.36.1",
"windows_i686_gnu 0.36.1",
"windows_i686_msvc 0.36.1",
"windows_x86_64_gnu 0.36.1",
"windows_x86_64_msvc 0.36.1",
]
[[package]]
name = "windows_aarch64_msvc"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29277a4435d642f775f63c7d1faeb927adba532886ce0287bd985bffb16b6bca"
[[package]]
name = "windows_aarch64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
[[package]]
name = "windows_gen"
version = "0.30.0"
@@ -5937,12 +5990,24 @@ version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1145e1989da93956c68d1864f32fb97c8f561a8f89a5125f6a2b7ea75524e4b8"
[[package]]
name = "windows_i686_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
[[package]]
name = "windows_i686_msvc"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4a09e3a0d4753b73019db171c1339cd4362c8c44baf1bcea336235e955954a6"
[[package]]
name = "windows_i686_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
[[package]]
name = "windows_macros"
version = "0.30.0"
@@ -5973,12 +6038,24 @@ version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ca64fcb0220d58db4c119e050e7af03c69e6f4f415ef69ec1773d9aab422d5a"
[[package]]
name = "windows_x86_64_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08cabc9f0066848fef4bc6a1c1668e6efce38b661d2aeec75d18d8617eebb5f1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
[[package]]
name = "winreg"
version = "0.7.0"
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "nym_wallet"
version = "1.0.7"
version = "1.0.9"
description = "Nym Native Wallet"
authors = ["Nym Technologies SA"]
license = ""
@@ -32,14 +32,14 @@ log = "0.4"
once_cell = "1.7.2"
pretty_env_logger = "0.4"
rand = "0.6.5"
reqwest = "0.11.9"
reqwest = {version = "0.11.9", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
strum = { version = "0.23", features = ["derive"] }
tauri = { version = "=1.0.0-rc.2", features = ["clipboard-all", "shell-open", "updater", "window-maximize"] }
tendermint-rpc = "0.23.0"
thiserror = "1.0"
tokio = { version = "1.10", features = ["sync", "time"] }
tokio = { version = "1.10", features = ["full"] }
toml = "0.5.8"
url = "2.2"
+1 -1
View File
@@ -552,7 +552,7 @@ api_url = 'https://baz/api'
.next()
.map(|v| v.nymd_url)
.unwrap();
assert_eq!(nymd_url.as_ref(), "https://rpc.nyx.nodes.guru/");
assert_eq!(nymd_url.as_ref(), "https://rpc.nymtech.net/");
let api_url = config
.get_base_validators(WalletNetwork::MAINNET)
+2
View File
@@ -58,6 +58,8 @@ fn main() {
mixnet::bond::unbond_gateway,
mixnet::bond::unbond_mixnode,
mixnet::bond::update_mixnode,
mixnet::bond::get_number_of_mixnode_delegators,
mixnet::bond::get_mix_node_description,
mixnet::delegate::delegate_to_mixnode,
mixnet::delegate::get_delegator_rewards,
mixnet::delegate::get_pending_delegation_events,
@@ -1,3 +1,5 @@
use std::time::Duration;
use crate::error::BackendError;
use crate::state::WalletState;
use crate::{Gateway, MixNode};
@@ -5,8 +7,18 @@ use nym_types::currency::DecCoin;
use nym_types::gateway::GatewayBond;
use nym_types::mixnode::MixNodeBond;
use nym_types::transaction::TransactionExecuteResult;
use reqwest::Error as ReqwestError;
use serde::{Deserialize, Serialize};
use validator_client::nymd::{Coin, Fee};
#[derive(Debug, Serialize, Deserialize)]
pub struct NodeDescription {
name: String,
description: String,
link: String,
location: String,
}
#[tauri::command]
pub async fn bond_gateway(
gateway: Gateway,
@@ -198,3 +210,51 @@ pub async fn get_operator_rewards(
);
Ok(display_coin)
}
#[tauri::command]
pub async fn get_number_of_mixnode_delegators(
identity: String,
state: tauri::State<'_, WalletState>,
) -> Result<usize, BackendError> {
let guard = state.read().await;
let client = guard.current_client()?;
let paged_delegations = client
.nymd
.get_mix_delegations_paged(identity, None, Some(20))
.await?;
Ok(paged_delegations.delegations.len())
}
async fn fetch_mix_node_description(
host: &str,
port: u16,
) -> Result<NodeDescription, ReqwestError> {
let milli_second = Duration::from_millis(1000);
let client = reqwest::Client::builder().timeout(milli_second).build()?;
let response = client
.get(format!("http://{}:{}/description", host, port))
.send()
.await;
match response {
Ok(res) => {
let json = res.json::<NodeDescription>().await;
match json {
Ok(json) => Ok(json),
Err(e) => Err(e),
}
}
Err(e) => Err(e),
}
}
#[tauri::command]
pub async fn get_mix_node_description(
host: &str,
port: u16,
) -> Result<NodeDescription, BackendError> {
return fetch_mix_node_description(host, port)
.await
.map_err(|e| BackendError::ReqwestError { source: e });
}
+2 -2
View File
@@ -516,7 +516,7 @@ mod tests {
"http://nymd_url.com/".parse().unwrap(),
"http://foo.com".parse().unwrap(),
"http://bar.com".parse().unwrap(),
"https://rpc.nyx.nodes.guru".parse().unwrap(),
"https://rpc.nymtech.net".parse().unwrap(),
],
);
assert_eq!(
@@ -538,7 +538,7 @@ mod tests {
"http://nymd_url.com/".parse().unwrap(),
"http://foo.com".parse().unwrap(),
"http://bar.com".parse().unwrap(),
"https://rpc.nyx.nodes.guru".parse().unwrap(),
"https://rpc.nymtech.net".parse().unwrap(),
],
)
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"package": {
"productName": "nym-wallet",
"version": "1.0.7"
"version": "1.0.9"
},
"build": {
"distDir": "../dist",
@@ -20,7 +20,7 @@ export const MultiAccountHowTo = ({ show, handleClose }: { show: boolean; handle
<Close />
</IconButton>
</Box>
<Typography variant="body1" sx={{ color: (t) => t.palette.nym.text.muted }}>
<Typography variant="body1" sx={{ color: (theme) => theme.palette.nym.text.muted }}>
How to set up multiple accounts
</Typography>
</DialogTitle>
@@ -29,7 +29,7 @@ export const MultiAccountHowTo = ({ show, handleClose }: { show: boolean; handle
<Alert
severity="warning"
icon={false}
sx={(t) => (t.palette.mode === 'dark' ? { bgcolor: (t) => t.palette.background.paper } : {})}
sx={(t) => (t.palette.mode === 'dark' ? { bgcolor: (theme) => theme.palette.background.paper } : {})}
>
<Typography>In order to create multiple accounts your wallet needs a password.</Typography>
<Typography>Follow steps below to create password.</Typography>
+42
View File
@@ -0,0 +1,42 @@
import React, { useRef } from 'react';
import { MoreVertSharp } from '@mui/icons-material';
import { IconButton, ListItemIcon, ListItemText, Menu, MenuItem } from '@mui/material';
export const ActionsMenu: React.FC<{ open: boolean; onOpen: () => void; onClose: () => void }> = ({
children,
open,
onOpen,
onClose,
}) => {
const anchorEl: any = useRef<HTMLElement>();
return (
<>
<IconButton ref={anchorEl} onClick={onOpen}>
<MoreVertSharp />
</IconButton>
<Menu anchorEl={anchorEl.current} open={open} onClose={onClose}>
{children}
</Menu>
</>
);
};
export const ActionsMenuItem = ({
title,
description,
onClick,
Icon,
disabled,
}: {
title: string;
description?: string;
onClick?: () => void;
Icon?: React.ReactNode;
disabled?: boolean;
}) => (
<MenuItem sx={{ p: 2 }} onClick={onClick} disabled={disabled}>
<ListItemIcon sx={{ color: 'text.primary' }}>{Icon}</ListItemIcon>
<ListItemText sx={{ color: 'text.primary' }} primary={title} secondary={description} />
</MenuItem>
);
+2 -13
View File
@@ -7,13 +7,11 @@ import ModeNightOutlinedIcon from '@mui/icons-material/ModeNightOutlined';
import LightModeOutlinedIcon from '@mui/icons-material/LightModeOutlined';
import { AppContext } from '../context/main';
import { NetworkSelector } from './NetworkSelector';
import { Node as NodeIcon } from '../svg-icons/node';
import { MultiAccounts } from './Accounts';
import { config } from '../config';
export const AppBar = () => {
const { logOut, handleShowTerminal, appEnv, handleShowSettings, showSettings, mode, handleSwitchMode } =
useContext(AppContext);
const { logOut, handleShowTerminal, appEnv, mode, handleSwitchMode } = useContext(AppContext);
const navigate = useNavigate();
return (
@@ -31,7 +29,7 @@ export const AppBar = () => {
<Grid item container justifyContent="flex-end" md={12} lg={5} spacing={2}>
<Grid item>
<IconButton size="small" onClick={handleSwitchMode} sx={{ color: 'text.primary' }}>
{mode === 'light' ? (
{mode === 'dark' ? (
<LightModeOutlinedIcon fontSize="small" />
) : (
<ModeNightOutlinedIcon fontSize="small" sx={{ transform: 'rotate(180deg)' }} />
@@ -45,15 +43,6 @@ export const AppBar = () => {
</IconButton>
</Grid>
)}
<Grid item>
<IconButton
onClick={handleShowSettings}
sx={{ color: showSettings ? 'primary.main' : 'text.primary' }}
size="small"
>
<NodeIcon fontSize="small" />
</IconButton>
</Grid>
<Grid item>
<IconButton
size="small"
@@ -1,65 +0,0 @@
import React, { useContext } from 'react';
import { Logout } from '@mui/icons-material';
import TerminalIcon from '@mui/icons-material/Terminal';
import ModeNightOutlinedIcon from '@mui/icons-material/ModeNightOutlined';
import LightModeOutlinedIcon from '@mui/icons-material/LightModeOutlined';
import { AppBar as MuiAppBar, Grid, IconButton, Toolbar } from '@mui/material';
import { Node } from 'src/svg-icons/node';
import { config } from '../../config';
import { AppContext } from '../../context/main';
import { MultiAccounts } from '../Accounts';
import { NetworkSelector } from '../NetworkSelector';
export const AppBar = () => {
const { showSettings, handleShowTerminal, appEnv, handleShowSettings, logOut, mode, handleSwitchMode } =
useContext(AppContext);
return (
<MuiAppBar position="sticky" sx={{ boxShadow: 'none', bgcolor: 'transparent', backgroundImage: 'none' }}>
<Toolbar disableGutters>
<Grid container justifyContent="space-between" alignItems="center" flexWrap="nowrap">
<Grid item container alignItems="center" spacing={1}>
<Grid item>
<MultiAccounts />
</Grid>
<Grid item>
<NetworkSelector />
</Grid>
</Grid>
<Grid item container justifyContent="flex-end" md={12} lg={5} spacing={2}>
<Grid item>
<IconButton size="small" onClick={handleSwitchMode} sx={{ color: 'text.primary' }}>
{mode === 'light' ? (
<ModeNightOutlinedIcon fontSize="small" />
) : (
<LightModeOutlinedIcon fontSize="small" />
)}
</IconButton>
</Grid>
{(appEnv?.SHOW_TERMINAL || config.IS_DEV_MODE) && (
<Grid item>
<IconButton size="small" onClick={handleShowTerminal} sx={{ color: 'text.primary' }}>
<TerminalIcon fontSize="small" />
</IconButton>
</Grid>
)}
<Grid item>
<IconButton
onClick={handleShowSettings}
sx={{ color: showSettings ? 'primary.main' : 'text.primary' }}
size="small"
>
<Node fontSize="small" />
</IconButton>
</Grid>
<Grid item>
<IconButton size="small" onClick={logOut} sx={{ color: 'text.primary' }}>
<Logout fontSize="small" />
</IconButton>
</Grid>
</Grid>
</Grid>
</Toolbar>
</MuiAppBar>
);
};
@@ -1 +0,0 @@
export * from './AppBar';
@@ -0,0 +1,44 @@
import React from 'react';
import { Box, Button, Typography } from '@mui/material';
import { NymCard } from '../NymCard';
export const Bond = ({
onBond,
disabled,
}: {
onBond: () => void;
disabled: boolean;
}) => (
<NymCard title="Bonding" borderless>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Typography>Bond a mixnode or a gateway</Typography>
<Box
sx={{
display: 'flex',
alignItems: 'flex-end',
justifyContent: 'space-between',
gap: 2,
}}
>
<Button
size="large"
variant="contained"
color="primary"
type="button"
disableElevation
onClick={onBond}
disabled={disabled}
>
Bond
</Button>
</Box>
</Box>
</NymCard>
);
@@ -0,0 +1,84 @@
import React from 'react';
import { Stack, Typography } from '@mui/material';
import { Link } from '@nymproject/react/link/Link';
import { TBondedGateway, urls } from 'src/context';
import { NymCard } from 'src/components';
import { Network } from 'src/types';
import { IdentityKey } from 'src/components/IdentityKey';
import { Cell, Header, NodeTable } from './NodeTable';
import { BondedGatewayActions, TBondedGatwayActions } from './BondedGatewayAction';
const headers: Header[] = [
{
header: 'IP',
id: 'ip',
sx: { pl: 0 },
},
{
header: 'Bond',
id: 'bond',
},
{
id: 'menu-button',
sx: { width: 34, maxWidth: 34 },
},
];
export const BondedGateway = ({
gateway,
network,
onActionSelect,
}: {
gateway: TBondedGateway;
network?: Network;
onActionSelect: (action: TBondedGatwayActions) => void;
}) => {
const { name, bond, ip, identityKey } = gateway;
const cells: Cell[] = [
{
cell: ip,
id: 'stake-saturation-cell',
},
{
cell: `${bond.amount} ${bond.denom}`,
id: 'stake-cell',
sx: { pl: 0 },
},
{
cell: <BondedGatewayActions onActionSelect={onActionSelect} />,
id: 'actions-cell',
align: 'right',
},
];
return (
<NymCard
borderless
title={
<Stack gap={2}>
<Typography variant="h5" fontWeight={600}>
Gateway
</Typography>
{name && (
<Typography fontWeight="regular" variant="h6">
{name}
</Typography>
)}
<IdentityKey identityKey={identityKey} />
</Stack>
}
>
<NodeTable headers={headers} cells={cells} />
{network && (
<Typography sx={{ mt: 2, fontSize: 'small' }}>
Check more stats of your node on the{' '}
<Link href={`${urls(network).networkExplorer}/network-components/gateways`} target="_blank">
explorer
</Link>
</Typography>
)}
</NymCard>
);
};
@@ -0,0 +1,31 @@
import React, { useState } from 'react';
import { ActionsMenu, ActionsMenuItem } from 'src/components/ActionsMenu';
import { Unbond as UnbondIcon } from '../../svg-icons';
export type TBondedGatwayActions = 'unbond';
export const BondedGatewayActions = ({
onActionSelect,
}: {
onActionSelect: (action: TBondedGatwayActions) => void;
}) => {
const [isOpen, setIsOpen] = useState(false);
const handleOpen = () => setIsOpen(true);
const handleClose = () => setIsOpen(false);
const handleActionClick = (action: TBondedGatwayActions) => {
onActionSelect(action);
handleClose();
};
return (
<ActionsMenu open={isOpen} onOpen={handleOpen} onClose={handleClose}>
<ActionsMenuItem
title="Unbond"
Icon={<UnbondIcon fontSize="inherit" />}
onClick={() => handleActionClick('unbond')}
/>
</ActionsMenu>
);
};
@@ -0,0 +1,138 @@
import React from 'react';
import { Box, Button, Stack, Typography } from '@mui/material';
import { Link } from '@nymproject/react/link/Link';
import { TBondedMixnode, urls } from 'src/context';
import { NymCard } from 'src/components';
import { Network } from 'src/types';
import { IdentityKey } from 'src/components/IdentityKey';
import { NodeStatus } from 'src/components/NodeStatus';
import { Node as NodeIcon } from '../../svg-icons/node';
import { Cell, Header, NodeTable } from './NodeTable';
import { BondedMixnodeActions, TBondedMixnodeActions } from './BondedMixnodeActions';
const headers: Header[] = [
{
header: 'Stake',
id: 'stake',
sx: { pl: 0 },
},
{
header: 'Bond',
id: 'bond',
},
{
header: 'Stake saturation',
id: 'stake-saturation',
},
{
header: 'PM',
id: 'profit-margin',
tooltipText:
'The percentage of the node rewards that you as the node operator will take before the rest of the reward is shared between you and the delegators.',
},
{
header: 'Operator rewards',
id: 'operator-rewards',
tooltipText:
'This is your (operator) new rewards including the PM and cost. You can compound your rewards manually every epoch or unbond your node to redeem them.',
},
{
header: 'No. delegators',
id: 'delegators',
},
{
id: 'menu-button',
sx: { width: 34, maxWidth: 34 },
},
];
export const BondedMixnode = ({
mixnode,
network,
onActionSelect,
}: {
mixnode: TBondedMixnode;
network?: Network;
onActionSelect: (action: TBondedMixnodeActions) => void;
}) => {
const { name, stake, bond, stakeSaturation, profitMargin, operatorRewards, delegators, status, identityKey } =
mixnode;
const cells: Cell[] = [
{
cell: `${stake.amount} ${stake.denom}`,
id: 'stake-cell',
},
{
cell: `${bond.amount} ${bond.denom}`,
id: 'bond-cell',
},
{
cell: `${stakeSaturation}%`,
id: 'stake-saturation-cell',
},
{
cell: `${profitMargin}%`,
id: 'pm-cell',
},
{
cell: `${operatorRewards.amount} ${operatorRewards.denom}`,
id: 'operator-rewards-cell',
},
{
cell: delegators,
id: 'delegators-cell',
},
{
cell: (
<BondedMixnodeActions
onActionSelect={onActionSelect}
disabledRedeemAndCompound={Number(mixnode.operatorRewards.amount) === 0}
/>
),
id: 'actions-cell',
align: 'right',
},
];
return (
<NymCard
borderless
title={
<Stack gap={2}>
<Box display="flex" alignItems="center" gap={2}>
<Typography variant="h5" fontWeight={600}>
Mix node
</Typography>
<NodeStatus status={status} />
</Box>
{name && (
<Typography fontWeight="regular" variant="h6">
{name}
</Typography>
)}
<IdentityKey identityKey={identityKey} />
</Stack>
}
Action={
<Button
variant="text"
color="secondary"
onClick={() => onActionSelect('nodeSettings')}
startIcon={<NodeIcon />}
>
Settings
</Button>
}
>
<NodeTable headers={headers} cells={cells} />
{network && (
<Typography sx={{ mt: 2, fontSize: 'small' }}>
Check more stats of your node on the{' '}
<Link href={`${urls(network).networkExplorer}/network-components/mixnodes`} target="_blank">
explorer
</Link>
</Typography>
)}
</NymCard>
);
};
@@ -0,0 +1,48 @@
import React, { useState } from 'react';
import { Typography } from '@mui/material';
import { ActionsMenu, ActionsMenuItem } from 'src/components/ActionsMenu';
import { Unbond as UnbondIcon } from '../../svg-icons';
export type TBondedMixnodeActions = 'nodeSettings' | 'bondMore' | 'unbond' | 'redeem' | 'compound';
export const BondedMixnodeActions = ({
onActionSelect,
disabledRedeemAndCompound,
}: {
onActionSelect: (action: TBondedMixnodeActions) => void;
disabledRedeemAndCompound: boolean;
}) => {
const [isOpen, setIsOpen] = useState(false);
const handleOpen = () => setIsOpen(true);
const handleClose = () => setIsOpen(false);
const handleActionClick = (action: TBondedMixnodeActions) => {
onActionSelect(action);
handleClose();
};
return (
<ActionsMenu open={isOpen} onOpen={handleOpen} onClose={handleClose}>
<ActionsMenuItem
title="Unbond"
Icon={<UnbondIcon fontSize="inherit" />}
onClick={() => handleActionClick('unbond')}
/>
<ActionsMenuItem
title="Compound rewards"
Icon={<Typography sx={{ pl: 1 }}>C</Typography>}
description={disabledRedeemAndCompound ? 'No rewards to compound' : 'Add your rewards to you balance'}
onClick={() => handleActionClick('compound')}
disabled={disabledRedeemAndCompound}
/>
<ActionsMenuItem
title="Redeem rewards"
Icon={<Typography sx={{ pl: 1 }}>R</Typography>}
description={disabledRedeemAndCompound ? 'No rewards to redeem' : 'Add your rewards to you balance'}
onClick={() => handleActionClick('redeem')}
disabled={disabledRedeemAndCompound}
/>
</ActionsMenu>
);
};
@@ -0,0 +1,50 @@
import React from 'react';
import {
Stack,
SxProps,
Table,
TableBody,
TableCell,
TableCellProps,
TableContainer,
TableHead,
TableRow,
Typography,
} from '@mui/material';
import { InfoTooltip } from '../InfoToolTip';
export type Header = { header?: string; id: string; tooltipText?: string; sx?: SxProps };
export type Cell = { cell: string | React.ReactNode; id: string; align?: TableCellProps['align']; sx?: SxProps };
export interface TableProps {
headers: Header[];
cells: Cell[];
}
export const NodeTable = ({ headers, cells }: TableProps) => (
<TableContainer>
<Table aria-label="node-table">
<TableHead>
<TableRow>
{headers.map(({ header, id, tooltipText }) => (
<TableCell key={id}>
<Stack direction="row" gap={1}>
{tooltipText && <InfoTooltip title={tooltipText} />}
<Typography>{header}</Typography>
</Stack>
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
<TableRow key="node-data">
{cells.map(({ cell, id, align }) => (
<TableCell key={id} align={align} sx={{ textTransform: 'uppercase' }}>
{cell}
</TableCell>
))}
</TableRow>
</TableBody>
</Table>
</TableContainer>
);
@@ -0,0 +1,214 @@
import React, { useEffect, useState } from 'react';
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
import { Box, Checkbox, FormControlLabel, Stack, TextField } from '@mui/material';
import { useForm } from 'react-hook-form';
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
import { NodeTypeSelector, TokenPoolSelector } from 'src/components';
import { yupResolver } from '@hookform/resolvers/yup';
import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from 'src/utils';
import { CurrencyDenom, TNodeType } from '@nymproject/types';
import { GatewayAmount, GatewayData } from 'src/pages/bonding/types';
import { gatewayValidationSchema, amountSchema } from './gatewayValidationSchema';
const NodeFormData = ({ gatewayData, onNext }: { gatewayData: GatewayData; onNext: (data: GatewayData) => void }) => {
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
const {
register,
formState: { errors },
handleSubmit,
setValue,
} = useForm({ resolver: yupResolver(gatewayValidationSchema), defaultValues: gatewayData });
const handleRequestValidation = (event: { detail: { step: number } }) => {
if (event.detail.step === 1) {
handleSubmit(onNext)();
}
};
useEffect(() => {
window.addEventListener('validate_bond_gateway_step' as any, handleRequestValidation);
return () => window.removeEventListener('validate_bond_gateway_step' as any, handleRequestValidation);
}, []);
return (
<Stack gap={2}>
<IdentityKeyFormField
required
fullWidth
label="Identity Key"
initialValue={gatewayData?.identityKey}
errorText={errors.identityKey?.message}
onChanged={(value) => setValue('identityKey', value)}
/>
<TextField
{...register('sphinxKey')}
name="sphinxKey"
label="Sphinx key"
error={Boolean(errors.sphinxKey)}
helperText={errors.sphinxKey?.message}
/>
<TextField
{...register('ownerSignature')}
name="ownerSignature"
label="Owner signature"
error={Boolean(errors.ownerSignature)}
helperText={errors.ownerSignature?.message}
/>
<TextField
{...register('location')}
name="location"
label="Location"
error={Boolean(errors.location)}
helperText={errors.location?.message}
required
sx={{ flexBasis: '50%' }}
/>
<Stack direction="row" gap={2}>
<TextField
{...register('host')}
name="host"
label="Host"
error={Boolean(errors.host)}
helperText={errors.host?.message}
required
sx={{ flexBasis: '50%' }}
/>
<TextField
{...register('version')}
name="version"
label="Version"
error={Boolean(errors.version)}
helperText={errors.version?.message}
required
sx={{ flexBasis: '50%' }}
/>
</Stack>
<FormControlLabel
control={<Checkbox onChange={() => setShowAdvancedOptions((show) => !show)} checked={showAdvancedOptions} />}
label="Show advanced options"
/>
{showAdvancedOptions && (
<Stack direction="row" gap={2} sx={{ mb: 2 }}>
<TextField
{...register('mixPort')}
name="mixPort"
label="Mix port"
error={Boolean(errors.mixPort)}
helperText={errors.mixPort?.message}
fullWidth
/>
<TextField
{...register('clientsPort')}
name="clientsPort"
label="Client WS API port"
error={Boolean(errors.clientsPort)}
helperText={errors.clientsPort?.message}
fullWidth
/>
</Stack>
)}
</Stack>
);
};
const AmountFormData = ({
denom,
amountData,
hasVestingTokens,
onNext,
}: {
denom: CurrencyDenom;
amountData: GatewayAmount;
hasVestingTokens: boolean;
onNext: (data: any) => void;
}) => {
const {
formState: { errors },
handleSubmit,
setValue,
getValues,
setError,
} = useForm({ resolver: yupResolver(amountSchema), defaultValues: amountData });
const handleRequestValidation = async (event: { detail: { step: number } }) => {
let hasSufficientTokens = true;
const values = getValues();
if (values.tokenPool === 'balance') {
hasSufficientTokens = await checkHasEnoughFunds(values.amount.amount);
}
if (values.tokenPool === 'locked') {
hasSufficientTokens = await checkHasEnoughLockedTokens(values.amount.amount);
}
if (event.detail.step === 2 && hasSufficientTokens) {
handleSubmit(onNext)();
} else {
setError('amount.amount', { message: 'Not enough tokens' });
}
};
useEffect(() => {
window.addEventListener('validate_bond_gateway_step' as any, handleRequestValidation);
return () => window.removeEventListener('validate_bond_gateway_step' as any, handleRequestValidation);
}, []);
return (
<Stack gap={2}>
<Box display="flex" gap={2} justifyContent="center" sx={{ mt: 2 }}>
{hasVestingTokens && <TokenPoolSelector disabled={false} onSelect={(pool) => setValue('tokenPool', pool)} />}
<CurrencyFormField
required
fullWidth
label="Amount"
autoFocus
onChanged={(newValue) => setValue('amount', newValue, { shouldValidate: true })}
validationError={errors.amount?.amount?.message}
denom={denom}
initialValue={amountData.amount.amount}
/>
</Box>
</Stack>
);
};
export const BondGatewayForm = ({
step,
denom,
gatewayData,
amountData,
hasVestingTokens,
onValidateGatewayData,
onValidateAmountData,
onSelectNodeType,
}: {
step: 1 | 2 | 3;
gatewayData: GatewayData;
amountData: GatewayAmount;
denom: CurrencyDenom;
hasVestingTokens: boolean;
onValidateGatewayData: (data: GatewayData) => void;
onValidateAmountData: (data: GatewayAmount) => Promise<void>;
onSelectNodeType: (nodeType: TNodeType) => void;
}) => (
<>
{step === 1 && (
<>
<Box sx={{ mb: 2 }}>
<NodeTypeSelector disabled={false} setNodeType={onSelectNodeType} nodeType="gateway" />
</Box>
<NodeFormData onNext={onValidateGatewayData} gatewayData={gatewayData} />
</>
)}
{step === 2 && (
<AmountFormData
denom={denom}
amountData={amountData}
hasVestingTokens={hasVestingTokens}
onNext={onValidateAmountData}
/>
)}
</>
);
@@ -0,0 +1,223 @@
import React, { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import { Box, Checkbox, FormControlLabel, Stack, TextField } from '@mui/material';
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
import { CurrencyDenom, TNodeType } from '@nymproject/types';
import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from 'src/utils';
import { NodeTypeSelector, TokenPoolSelector } from 'src/components';
import { MixnodeAmount, MixnodeData } from 'src/pages/bonding/types';
import { amountSchema, mixnodeValidationSchema } from './mixnodeValidationSchema';
const NodeFormData = ({ mixnodeData, onNext }: { mixnodeData: MixnodeData; onNext: (data: any) => void }) => {
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
const {
register,
formState: { errors },
handleSubmit,
setValue,
} = useForm({ resolver: yupResolver(mixnodeValidationSchema), defaultValues: mixnodeData });
const handleRequestValidation = (event: { detail: { step: number } }) => {
if (event.detail.step === 1) {
handleSubmit(onNext)();
}
};
useEffect(() => {
window.addEventListener('validate_bond_mixnode_step' as any, handleRequestValidation);
return () => window.removeEventListener('validate_bond_mixnode_step' as any, handleRequestValidation);
}, []);
return (
<Stack gap={2}>
<IdentityKeyFormField
required
fullWidth
label="Identity Key"
initialValue={mixnodeData?.identityKey}
errorText={errors.identityKey?.message}
onChanged={(value) => setValue('identityKey', value)}
/>
<TextField
{...register('sphinxKey')}
name="sphinxKey"
label="Sphinx key"
error={Boolean(errors.sphinxKey)}
helperText={errors.sphinxKey?.message}
/>
<TextField
{...register('ownerSignature')}
name="ownerSignature"
label="Owner signature"
error={Boolean(errors.ownerSignature)}
helperText={errors.ownerSignature?.message}
/>
<Stack direction="row" gap={2}>
<TextField
{...register('host')}
name="host"
label="Host"
error={Boolean(errors.host)}
helperText={errors.host?.message}
required
sx={{ flexBasis: '50%' }}
/>
<TextField
{...register('version')}
name="version"
label="Version"
error={Boolean(errors.version)}
helperText={errors.version?.message}
required
sx={{ flexBasis: '50%' }}
/>
</Stack>
<FormControlLabel
control={<Checkbox onChange={() => setShowAdvancedOptions((show) => !show)} checked={showAdvancedOptions} />}
label="Show advanced options"
/>
{showAdvancedOptions && (
<Stack direction="row" gap={2} sx={{ mb: 2 }}>
<TextField
{...register('mixPort')}
name="mixPort"
label="Mix port"
error={Boolean(errors.mixPort)}
helperText={errors.mixPort?.message}
fullWidth
/>
<TextField
{...register('verlocPort')}
name="verlocPort"
label="Verloc port"
error={Boolean(errors.verlocPort)}
helperText={errors.verlocPort?.message}
fullWidth
/>
<TextField
{...register('httpApiPort')}
name="httpApiPort"
label="HTTP api port"
error={Boolean(errors.httpApiPort)}
helperText={errors.httpApiPort?.message}
fullWidth
/>
</Stack>
)}
</Stack>
);
};
const AmountFormData = ({
amountData,
hasVestingTokens,
denom,
onNext,
}: {
amountData: MixnodeAmount;
hasVestingTokens: boolean;
denom: CurrencyDenom;
onNext: (data: MixnodeAmount) => void;
}) => {
const {
register,
formState: { errors },
handleSubmit,
setValue,
getValues,
setError,
} = useForm({ resolver: yupResolver(amountSchema), defaultValues: amountData });
const handleRequestValidation = async (event: { detail: { step: number } }) => {
let hasSufficientTokens = true;
const values = getValues();
if (values.tokenPool === 'balance') {
hasSufficientTokens = await checkHasEnoughFunds(values.amount.amount);
}
if (values.tokenPool === 'locked') {
hasSufficientTokens = await checkHasEnoughLockedTokens(values.amount.amount);
}
if (event.detail.step === 2 && hasSufficientTokens) {
handleSubmit(onNext)();
} else {
setError('amount.amount', { message: 'Not enough tokens' });
}
};
useEffect(() => {
window.addEventListener('validate_bond_mixnode_step' as any, handleRequestValidation);
return () => window.removeEventListener('validate_bond_mixnode_step' as any, handleRequestValidation);
}, []);
return (
<Stack gap={2}>
<Box display="flex" gap={2} justifyContent="center" sx={{ mt: 2 }}>
{hasVestingTokens && <TokenPoolSelector disabled={false} onSelect={(pool) => setValue('tokenPool', pool)} />}
<CurrencyFormField
required
fullWidth
label="Amount"
autoFocus
onChanged={(newValue) => {
setValue('amount', newValue, { shouldValidate: true });
}}
validationError={errors.amount?.amount?.message}
denom={denom}
initialValue={amountData.amount.amount}
/>
</Box>
<TextField
{...register('profitMargin')}
name="profitMargin"
label="Profit margin"
error={Boolean(errors.profitMargin)}
helperText={errors.profitMargin?.message}
/>
</Stack>
);
};
export const BondMixnodeForm = ({
step,
denom,
mixnodeData,
amountData,
hasVestingTokens,
onValidateMixnodeData,
onValidateAmountData,
onSelectNodeType,
}: {
step: 1 | 2 | 3;
mixnodeData: MixnodeData;
amountData: MixnodeAmount;
denom: CurrencyDenom;
hasVestingTokens: boolean;
onValidateMixnodeData: (data: MixnodeData) => void;
onValidateAmountData: (data: MixnodeAmount) => Promise<void>;
onSelectNodeType: (nodeType: TNodeType) => void;
}) => (
<>
{step === 1 && (
<>
<Box sx={{ mb: 2 }}>
<NodeTypeSelector disabled={false} setNodeType={onSelectNodeType} nodeType="mixnode" />
</Box>
<NodeFormData onNext={onValidateMixnodeData} mixnodeData={mixnodeData} />
</>
)}
{step === 2 && (
<AmountFormData
denom={denom}
amountData={amountData}
hasVestingTokens={hasVestingTokens}
onNext={onValidateAmountData}
/>
)}
</>
);
@@ -0,0 +1,59 @@
import * as Yup from 'yup';
import {
isValidHostname,
validateAmount,
validateKey,
validateLocation,
validateRawPort,
validateVersion,
} from 'src/utils';
export const gatewayValidationSchema = Yup.object().shape({
identityKey: Yup.string()
.required('An indentity key is required')
.test('valid-id-key', 'A valid identity key is required', (value) => validateKey(value || '', 32)),
sphinxKey: Yup.string()
.required('A sphinx key is required')
.test('valid-sphinx-key', 'A valid sphinx key is required', (value) => validateKey(value || '', 32)),
ownerSignature: Yup.string()
.required('Signature is required')
.test('valid-signature', 'A valid signature is required', (value) => validateKey(value || '', 64)),
host: Yup.string()
.required('A host is required')
.test('valid-host', 'A valid host is required', (value) => (value ? isValidHostname(value) : false)),
version: Yup.string()
.required('A version is required')
.test('valid-version', 'A valid version is required', (value) => (value ? validateVersion(value) : false)),
location: Yup.string()
.required('A location is required')
.test('valid-location', 'A valid version is required', (locationValueTest) =>
locationValueTest ? validateLocation(locationValueTest) : false,
),
mixPort: Yup.number()
.required('A mixport is required')
.test('valid-mixport', 'A valid mixport is required', (value) => (value ? validateRawPort(value) : false)),
clientsPort: Yup.number()
.required('A clients port is required')
.test('valid-clients', 'A valid clients port is required', (value) => (value ? validateRawPort(value) : false)),
});
export const amountSchema = Yup.object().shape({
amount: Yup.object().shape({
amount: Yup.string()
.required('An amount is required')
.test('valid-amount', 'Pledge error', async function isValidAmount(this, value) {
const isValid = await validateAmount(value || '', '100');
if (!isValid) {
return this.createError({ message: 'A valid amount is required (min 100)' });
}
return true;
}),
}),
});
@@ -0,0 +1,51 @@
import * as Yup from 'yup';
import { isValidHostname, validateAmount, validateKey, validateRawPort, validateVersion } from 'src/utils';
export const mixnodeValidationSchema = Yup.object().shape({
identityKey: Yup.string()
.required('An identity key is required')
.test('valid-id-key', 'A valid identity key is required', (value) => validateKey(value || '', 32)),
sphinxKey: Yup.string()
.required('A sphinx key is required')
.test('valid-sphinx-key', 'A valid sphinx key is required', (value) => validateKey(value || '', 32)),
ownerSignature: Yup.string()
.required('Signature is required')
.test('valid-signature', 'A valid signature is required', (value) => validateKey(value || '', 64)),
host: Yup.string()
.required('A host is required')
.test('valid-host', 'A valid host is required', (value) => (value ? isValidHostname(value) : false)),
version: Yup.string()
.required('A version is required')
.test('valid-version', 'A valid version is required', (value) => (value ? validateVersion(value) : false)),
mixPort: Yup.number()
.required('A mixport is required')
.test('valid-mixport', 'A valid mixport is required', (value) => (value ? validateRawPort(value) : false)),
verlocPort: Yup.number()
.required('A verloc port is required')
.test('valid-verloc', 'A valid verloc port is required', (value) => (value ? validateRawPort(value) : false)),
httpApiPort: Yup.number()
.required('A http-api port is required')
.test('valid-http', 'A valid http-api port is required', (value) => (value ? validateRawPort(value) : false)),
});
export const amountSchema = Yup.object().shape({
amount: Yup.object().shape({
amount: Yup.string()
.required('An amount is required')
.test('valid-amount', 'Pledge error', async function isValidAmount(this, value) {
const isValid = await validateAmount(value || '', '100');
if (!isValid) {
return this.createError({ message: 'A valid amount is required (min 100)' });
}
return true;
}),
}),
profitMargin: Yup.number().required('Profit Percentage is required').min(0).max(100),
});
@@ -0,0 +1,161 @@
import React, { useEffect, useState } from 'react';
import { Box } from '@mui/material';
import { CurrencyDenom, TNodeType } from '@nymproject/types';
import { ConfirmTx } from 'src/components/ConfirmTX';
import { ModalListItem } from 'src/components/Modals/ModalListItem';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { TPoolOption } from 'src/components/TokenPoolSelector';
import { useGetFee } from 'src/hooks/useGetFee';
import { GatewayAmount, GatewayData } from 'src/pages/bonding/types';
import { simulateBondGateway, simulateVestingBondGateway } from 'src/requests';
import { TBondGatewayArgs } from 'src/types';
import { BondGatewayForm } from '../forms/BondGatewayForm';
const defaultMixnodeValues: GatewayData = {
identityKey: '',
sphinxKey: '',
ownerSignature: '',
location: '',
host: '',
version: '',
mixPort: 1789,
clientsPort: 1790,
};
const defaultAmountValues = (denom: CurrencyDenom) => ({
amount: { amount: '100', denom },
tokenPool: 'balance',
});
export const BondGatewayModal = ({
denom,
hasVestingTokens,
onBondGateway,
onSelectNodeType,
onClose,
onError,
}: {
denom: CurrencyDenom;
hasVestingTokens: boolean;
onBondGateway: (data: TBondGatewayArgs, tokenPool: TPoolOption) => void;
onSelectNodeType: (type: TNodeType) => void;
onClose: () => void;
onError: (e: string) => void;
}) => {
const [step, setStep] = useState<1 | 2 | 3>(1);
const [gatewayData, setGatewayData] = useState<GatewayData>(defaultMixnodeValues);
const [amountData, setAmountData] = useState<GatewayAmount>(defaultAmountValues(denom));
const { fee, getFee, resetFeeState, feeError } = useGetFee();
useEffect(() => {
if (feeError) {
onError(feeError);
}
}, [feeError]);
const validateStep = async (s: number) => {
const event = new CustomEvent('validate_bond_gateway_step', { detail: { step: s } });
window.dispatchEvent(event);
};
const handleBack = () => {
setStep(1);
};
const handleUpdateGatwayData = (data: GatewayData) => {
setGatewayData(data);
setStep(2);
};
const handleUpdateAmountData = async (data: GatewayAmount) => {
setAmountData(data);
const payload = {
pledge: data.amount,
ownerSignature: gatewayData.ownerSignature,
gateway: {
...gatewayData,
host: gatewayData.host,
version: gatewayData.version,
mix_port: gatewayData.mixPort,
clients_port: gatewayData.clientsPort,
sphinx_key: gatewayData.sphinxKey,
identity_key: gatewayData.identityKey,
location: gatewayData.location,
},
};
if (data.tokenPool === 'balance') {
await getFee<TBondGatewayArgs>(simulateBondGateway, payload);
} else {
await getFee<TBondGatewayArgs>(simulateVestingBondGateway, payload);
}
};
const handleConfirm = async () => {
await onBondGateway(
{
pledge: amountData.amount,
ownerSignature: gatewayData.ownerSignature,
gateway: {
...gatewayData,
host: gatewayData.host,
version: gatewayData.version,
mix_port: gatewayData.mixPort,
clients_port: gatewayData.clientsPort,
sphinx_key: gatewayData.sphinxKey,
identity_key: gatewayData.identityKey,
location: gatewayData.location,
},
},
amountData.tokenPool as TPoolOption,
);
};
if (fee) {
return (
<ConfirmTx
open
header="Bond details"
fee={fee}
onClose={onClose}
onPrev={resetFeeState}
onConfirm={handleConfirm}
>
<ModalListItem label="Node identity key" value={gatewayData.identityKey} divider />
<ModalListItem
label="Amount"
value={`${amountData.amount.amount} ${amountData.amount.denom.toUpperCase()}`}
divider
/>
</ConfirmTx>
);
}
return (
<SimpleModal
open
onOk={async () => {
await validateStep(step);
}}
onBack={step === 2 ? handleBack : undefined}
onClose={onClose}
header="Bond gateway"
subHeader={`Step ${step}/2`}
okLabel="Next"
>
<Box sx={{ mb: 2 }}>
<BondGatewayForm
step={step}
denom={denom}
gatewayData={gatewayData}
amountData={amountData}
hasVestingTokens={hasVestingTokens}
onValidateGatewayData={handleUpdateGatwayData}
onValidateAmountData={handleUpdateAmountData}
onSelectNodeType={onSelectNodeType}
/>
</Box>
</SimpleModal>
);
};
@@ -0,0 +1,160 @@
import React, { useEffect, useState } from 'react';
import { Box } from '@mui/material';
import { CurrencyDenom, TNodeType } from '@nymproject/types';
import { ConfirmTx } from 'src/components/ConfirmTX';
import { ModalListItem } from 'src/components/Modals/ModalListItem';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { TPoolOption } from 'src/components/TokenPoolSelector';
import { useGetFee } from 'src/hooks/useGetFee';
import { MixnodeAmount, MixnodeData } from 'src/pages/bonding/types';
import { simulateBondMixnode, simulateVestingBondMixnode } from 'src/requests';
import { TBondMixNodeArgs } from 'src/types';
import { BondMixnodeForm } from '../forms/BondMixnodeForm';
const defaultMixnodeValues: MixnodeData = {
identityKey: '',
sphinxKey: '',
ownerSignature: '',
host: '',
version: '',
mixPort: 1789,
verlocPort: 1790,
httpApiPort: 8000,
};
const defaultAmountValues = (denom: CurrencyDenom) => ({
amount: { amount: '100', denom },
profitMargin: 10,
tokenPool: 'balance',
});
export const BondMixnodeModal = ({
denom,
hasVestingTokens,
onBondMixnode,
onSelectNodeType,
onClose,
onError,
}: {
denom: CurrencyDenom;
hasVestingTokens: boolean;
onBondMixnode: (data: TBondMixNodeArgs, tokenPool: TPoolOption) => void;
onSelectNodeType: (type: TNodeType) => void;
onClose: () => void;
onError: (e: string) => void;
}) => {
const [step, setStep] = useState<1 | 2 | 3>(1);
const [mixnodeData, setMixnodeData] = useState<MixnodeData>(defaultMixnodeValues);
const [amountData, setAmountData] = useState<MixnodeAmount>(defaultAmountValues(denom));
const { fee, getFee, resetFeeState, feeError } = useGetFee();
useEffect(() => {
if (feeError) {
onError(feeError);
}
}, [feeError]);
const validateStep = async (s: number) => {
const event = new CustomEvent('validate_bond_mixnode_step', { detail: { step: s } });
window.dispatchEvent(event);
};
const handleBack = () => {
setStep(1);
};
const handleUpdateMixnodeData = (data: MixnodeData) => {
setMixnodeData(data);
setStep(2);
};
const handleUpdateAmountData = async (data: MixnodeAmount) => {
setAmountData(data);
const payload = {
pledge: data.amount,
ownerSignature: mixnodeData.ownerSignature,
mixnode: {
...mixnodeData,
mix_port: mixnodeData.mixPort,
http_api_port: mixnodeData.httpApiPort,
verloc_port: mixnodeData.verlocPort,
sphinx_key: mixnodeData.sphinxKey,
identity_key: mixnodeData.identityKey,
profit_margin_percent: data.profitMargin,
},
};
if (data.tokenPool === 'balance') {
await getFee<TBondMixNodeArgs>(simulateBondMixnode, payload);
} else {
await getFee<TBondMixNodeArgs>(simulateVestingBondMixnode, payload);
}
};
const handleConfirm = async () => {
await onBondMixnode(
{
pledge: amountData.amount,
ownerSignature: mixnodeData.ownerSignature,
mixnode: {
...mixnodeData,
mix_port: mixnodeData.mixPort,
http_api_port: mixnodeData.httpApiPort,
verloc_port: mixnodeData.verlocPort,
sphinx_key: mixnodeData.sphinxKey,
identity_key: mixnodeData.identityKey,
profit_margin_percent: amountData.profitMargin,
},
},
amountData.tokenPool as TPoolOption,
);
};
if (fee) {
return (
<ConfirmTx
open
header="Bond details"
fee={fee}
onClose={onClose}
onPrev={resetFeeState}
onConfirm={handleConfirm}
>
<ModalListItem label="Node identity key" value={mixnodeData.identityKey} divider />
<ModalListItem
label="Amount"
value={`${amountData.amount.amount} ${amountData.amount.denom.toUpperCase()}`}
divider
/>
</ConfirmTx>
);
}
return (
<SimpleModal
open
onOk={async () => {
await validateStep(step);
}}
onBack={step === 2 ? handleBack : undefined}
onClose={onClose}
header="Bond mixnode"
subHeader={`Step ${step}/2`}
okLabel="Next"
>
<Box sx={{ mb: 2 }}>
<BondMixnodeForm
step={step}
denom={denom}
mixnodeData={mixnodeData}
amountData={amountData}
hasVestingTokens={hasVestingTokens}
onValidateMixnodeData={handleUpdateMixnodeData}
onValidateAmountData={handleUpdateAmountData}
onSelectNodeType={onSelectNodeType}
/>
</Box>
</SimpleModal>
);
};
@@ -0,0 +1,115 @@
import React, { useEffect, useState } from 'react';
import { Box, FormHelperText, Stack, TextField } from '@mui/material';
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
import { ModalListItem } from 'src/components/Modals/ModalListItem';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { DecCoin } from '@nymproject/types';
import { TokenPoolSelector, TPoolOption } from 'src/components/TokenPoolSelector';
import { ConfirmTx } from 'src/components/ConfirmTX';
import { useGetFee } from 'src/hooks/useGetFee';
import { validateAmount, validateKey } from 'src/utils';
export const BondMoreModal = ({
currentBond,
userBalance,
hasVestingTokens,
onConfirm,
onClose,
}: {
currentBond: DecCoin;
userBalance?: string;
hasVestingTokens: boolean;
onConfirm: (args: { additionalBond: DecCoin; signature: string; tokenPool: TPoolOption }) => Promise<void>;
onClose: () => void;
}) => {
const { fee, resetFeeState } = useGetFee();
const [additionalBond, setAdditionalBond] = useState<DecCoin>({ amount: '0', denom: currentBond.denom });
const [signature, setSignature] = useState<string>('');
const [tokenPool, setTokenPool] = useState<TPoolOption>('balance');
const [errorAmount, setErrorAmount] = useState(false);
const [errorSignature, setErrorSignature] = useState(false);
const handleOnOk = async () => {
const errors = {
amount: false,
signature: false,
};
if (!validateKey(signature || '', 64)) {
errors.signature = true;
}
if (!additionalBond?.amount) {
errors.amount = true;
}
if (additionalBond && !(await validateAmount(additionalBond.amount, '1'))) {
errors.amount = true;
}
if (!errors.amount && !errors.signature) {
onConfirm({ additionalBond, signature, tokenPool });
} else {
setErrorAmount(errors.amount);
setErrorSignature(errors.signature);
}
};
useEffect(() => {
setErrorAmount(false);
}, [additionalBond]);
if (fee)
return (
<ConfirmTx
header="Bond more details"
open
fee={fee}
onConfirm={async () => onConfirm({ additionalBond, signature, tokenPool })}
onPrev={resetFeeState}
>
<ModalListItem label="Current bond" value={`${currentBond.amount} ${currentBond.denom}`} divider />
<ModalListItem label="Additional bond" value={`${additionalBond?.amount} ${additionalBond?.denom}`} divider />
</ConfirmTx>
);
return (
<SimpleModal
open
header="Bond more"
subHeader="Bond more tokens on your node and receive more rewards"
okLabel="Next"
onOk={handleOnOk}
okDisabled={errorAmount || errorSignature}
onClose={onClose}
>
<Stack gap={2}>
<Box display="flex" gap={1}>
{hasVestingTokens && <TokenPoolSelector disabled={false} onSelect={(pool) => setTokenPool(pool)} />}
<CurrencyFormField
autoFocus
label="Bond amount"
denom={currentBond.denom}
onChanged={(value) => {
setAdditionalBond(value);
setErrorSignature(false);
}}
fullWidth
validationError={errorAmount ? 'Please enter a valid amount' : undefined}
/>
</Box>
<Box>
<TextField fullWidth label="Signature" value={signature} onChange={(e) => setSignature(e.target.value)} />
{errorSignature && <FormHelperText sx={{ color: 'error.main' }}>Invalid signature</FormHelperText>}
</Box>
<Box>
<ModalListItem label="Account balance" value={userBalance?.toUpperCase() || '-'} divider />
<ModalListItem label="Current bond" value={`${currentBond.amount} ${currentBond.denom}`} divider />
<ModalListItem label="Est. fee for this operation will be calculated in the next page" value="" divider />
</Box>
</Stack>
</SimpleModal>
);
};
@@ -0,0 +1,53 @@
import React, { useEffect } from 'react';
import { FeeDetails } from '@nymproject/types';
import { ModalFee } from 'src/components/Modals/ModalFee';
import { ModalListItem } from 'src/components/Modals/ModalListItem';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { TBondedMixnode } from 'src/context';
import { useGetFee } from 'src/hooks/useGetFee';
import { simulateCompoundOperatorReward, simulateVestingCompoundOperatorReward } from 'src/requests';
export const CompoundRewardsModal = ({
node,
onConfirm,
onClose,
onError,
}: {
node: TBondedMixnode;
onClose: () => void;
onConfirm: (fee?: FeeDetails) => void;
onError: (err: string) => void;
}) => {
const { fee, getFee, feeError, isFeeLoading } = useGetFee();
useEffect(() => {
if (feeError) onError(feeError);
}, [feeError]);
useEffect(() => {
if (node.proxy) getFee(simulateVestingCompoundOperatorReward, {});
else getFee(simulateCompoundOperatorReward, {});
}, []);
const handleOnOK = async () => onConfirm(fee);
return (
<SimpleModal
open
header="Compound rewards"
subHeader="Get more rewards by compounding"
okLabel="Compound"
okDisabled={isFeeLoading}
onOk={handleOnOK}
onClose={onClose}
>
<ModalListItem
label="Rewards to redeem"
value={`${node.operatorRewards.amount} ${node.operatorRewards.denom.toUpperCase()}`}
divider
/>
<ModalFee fee={fee} isLoading={isFeeLoading} divider />
<ModalListItem label="Rewards will be transferred to the account you are logged in with" value="" />
</SimpleModal>
);
};
@@ -0,0 +1,52 @@
import React from 'react';
import { Stack, Typography, SxProps } from '@mui/material';
import { Link } from '@nymproject/react/link/Link';
import { ConfirmationModal } from 'src/components/Modals/ConfirmationModal';
import { ErrorModal } from 'src/components/Modals/ErrorModal';
export type ConfirmationDetailProps = {
status: 'success' | 'error';
title: string;
subtitle?: string;
txUrl?: string;
};
export const ConfirmationDetailsModal = ({
title,
subtitle,
txUrl,
status,
onClose,
sx,
backdropProps,
}: ConfirmationDetailProps & {
onClose: () => void;
sx?: SxProps;
backdropProps?: object;
}) => {
if (status === 'error') {
<ErrorModal open message={subtitle} onClose={onClose} />;
}
return (
<ConfirmationModal
open
onConfirm={onClose}
onClose={onClose}
title=""
confirmButton="Done"
maxWidth="xs"
fullWidth
sx={sx}
backdropProps={backdropProps}
>
<Stack alignItems="center" spacing={2}>
<Typography variant="h6" fontWeight={600}>
{title}
</Typography>
<Typography>{subtitle}</Typography>
{txUrl && <Link href={txUrl} target="_blank" sx={{ ml: 1 }} text="View on blockchain" />}
</Stack>
</ConfirmationModal>
);
};
@@ -0,0 +1,128 @@
import React, { useEffect, useState } from 'react';
import { Box, Button, FormHelperText, TextField, Typography } from '@mui/material';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { Node as NodeIcon } from 'src/svg-icons/node';
import { TBondedMixnode } from 'src/context';
import { Tabs } from 'src/components/Tabs';
import { ModalListItem } from 'src/components/Modals/ModalListItem';
import { isDecimal } from 'src/utils';
import { useGetFee } from 'src/hooks/useGetFee';
import { ConfirmTx } from 'src/components/ConfirmTX';
import { simulateUpdateMixnode, simulateVestingUpdateMixnode } from 'src/requests';
import { LoadingModal } from 'src/components/Modals/LoadingModal';
import { FeeDetails } from '@nymproject/types';
export const NodeSettings = ({
currentPm,
isVesting,
onConfirm,
onClose,
onError,
}: {
currentPm: TBondedMixnode['profitMargin'];
isVesting: boolean;
onConfirm: (profitMargin: number, fee?: FeeDetails) => Promise<void>;
onClose: () => void;
onError: (err: string) => void;
}) => {
const [pm, setPm] = useState(currentPm.toString());
const [error, setError] = useState(false);
const { fee, getFee, resetFeeState, isFeeLoading, feeError } = useGetFee();
const handleValidate = async () => {
let isValid = true;
const pmAsNumber = Number(pm);
if (!pmAsNumber) {
isValid = false;
}
if (isDecimal(pmAsNumber)) {
isValid = false;
}
if (pmAsNumber > 100) {
isValid = false;
}
if (pmAsNumber < 0) {
isValid = false;
}
if (!isValid) {
setError(true);
return;
}
if (isVesting) {
await getFee(simulateVestingUpdateMixnode, { profitMarginPercent: pmAsNumber });
} else {
await getFee(simulateUpdateMixnode, { profitMarginPercent: pmAsNumber });
}
};
useEffect(() => {
setError(false);
}, [pm]);
useEffect(() => {
if (feeError) {
onError(feeError);
}
}, [feeError]);
if (isFeeLoading) return <LoadingModal />;
if (fee)
return (
<ConfirmTx
open
header="Profit margin change"
fee={fee}
onPrev={resetFeeState}
onClose={onClose}
onConfirm={() => onConfirm(Number(pm), fee)}
>
<ModalListItem label="Current profit margin" value={`${currentPm}%`} divider />
<ModalListItem label="New profit margin" value={`${pm}%`} divider />
</ConfirmTx>
);
return (
<SimpleModal
open
hideCloseIcon
sx={{ p: 0 }}
header={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, p: 3 }}>
<NodeIcon />
<Typography variant="h6" fontWeight={600}>
Node Settings
</Typography>
</Box>
}
okLabel="Next"
onClose={onClose}
>
<Tabs tabs={['System variables']} selectedTab={0} disableActiveTabHighlight />
<Box sx={{ p: 3 }}>
<Typography fontWeight={600} sx={{ mb: 1 }}>
Set profit margin
</Typography>
<Box sx={{ mb: 3 }}>
<TextField placeholder="Profit margin" value={pm} onChange={(e) => setPm(e.target.value)} fullWidth />
{error && (
<FormHelperText sx={{ color: 'error.main' }}>
Profit margin should be a whole number between 0 and 100
</FormHelperText>
)}
<FormHelperText>Your new profit margin will be applied in the next epoch</FormHelperText>
</Box>
<Box sx={{ mb: 3 }}>
<ModalListItem label="Est. fee for this operation will be caculated in the next page" value="" />
</Box>
<Button variant="contained" fullWidth size="large" onClick={handleValidate} disabled={error}>
Next
</Button>
</Box>
</SimpleModal>
);
};
@@ -0,0 +1,53 @@
import React, { useEffect } from 'react';
import { FeeDetails } from '@nymproject/types';
import { ModalListItem } from 'src/components/Modals/ModalListItem';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { ModalFee } from 'src/components/Modals/ModalFee';
import { useGetFee } from 'src/hooks/useGetFee';
import { simulateClaimOperatorReward, simulateVestingClaimOperatorReward } from 'src/requests';
import { TBondedMixnode } from 'src/context';
export const RedeemRewardsModal = ({
node,
onConfirm,
onError,
onClose,
}: {
node: TBondedMixnode;
onConfirm: (fee?: FeeDetails) => Promise<void>;
onError: (err: string) => void;
onClose: () => void;
}) => {
const { fee, getFee, isFeeLoading, feeError } = useGetFee();
useEffect(() => {
if (feeError) onError(feeError);
}, [feeError]);
useEffect(() => {
if (node.proxy) getFee(simulateVestingClaimOperatorReward, {});
else getFee(simulateClaimOperatorReward, {});
}, []);
const handleOnOK = async () => onConfirm(fee);
return (
<SimpleModal
open
header="Redeem rewards"
subHeader="Claim you rewards"
okLabel="Redeem"
okDisabled={isFeeLoading}
onOk={handleOnOK}
onClose={onClose}
>
<ModalListItem
label="Rewards to redeem"
value={`${node.operatorRewards.amount} ${node.operatorRewards.denom.toUpperCase()}`}
divider
/>
<ModalFee fee={fee} isLoading={isFeeLoading} divider />
<ModalListItem label="Rewards will be transferred to the account you are logged in with" value="" />
</SimpleModal>
);
};
@@ -0,0 +1,72 @@
import * as React from 'react';
import { Typography } from '@mui/material';
import { useEffect } from 'react';
import { TBondedGateway, TBondedMixnode } from 'src/context';
import { useGetFee } from 'src/hooks/useGetFee';
import { isGateway, isMixnode } from 'src/types';
import { ModalFee } from '../../Modals/ModalFee';
import { ModalListItem } from '../../Modals/ModalListItem';
import { SimpleModal } from '../../Modals/SimpleModal';
import {
simulateUnbondGateway,
simulateUnbondMixnode,
simulateVestingUnbondGateway,
simulateVestingUnbondMixnode,
} from '../../../requests';
interface Props {
node: TBondedMixnode | TBondedGateway;
onConfirm: () => Promise<void>;
onClose: () => void;
onError: (e: string) => void;
}
export const UnbondModal = ({ node, onConfirm, onClose, onError }: Props) => {
const { fee, isFeeLoading, getFee, feeError } = useGetFee();
useEffect(() => {
if (feeError) {
onError(feeError);
}
}, [feeError]);
useEffect(() => {
if (isMixnode(node) && !node.proxy) {
getFee(simulateUnbondMixnode, {});
}
if (isMixnode(node) && node.proxy) {
getFee(simulateVestingUnbondMixnode, {});
}
if (isGateway(node) && !node.proxy) {
getFee(simulateUnbondGateway, {});
}
if (isGateway(node) && node.proxy) {
getFee(simulateVestingUnbondGateway, {});
}
}, [node]);
return (
<SimpleModal
open
header="Unbond"
subHeader="Unbond and remove your node from the mixnet"
okLabel="Unbond"
onOk={onConfirm}
onClose={onClose}
>
<ModalListItem label="Amount to unbond" value={`${node.bond.amount} ${node.bond.denom.toUpperCase()}`} divider />
{isMixnode(node) && (
<ModalListItem
label="Operator rewards"
value={`${node.operatorRewards.amount} ${node.operatorRewards.denom.toUpperCase()}`}
divider
/>
)}
<ModalFee isLoading={isFeeLoading} fee={fee} divider />
<Typography fontSize="small">Tokens will be transferred to the account you are logged in with now</Typography>
</SimpleModal>
);
};
@@ -10,9 +10,9 @@ export default {
const Template: ComponentStory<typeof ConfirmTx> = (args) => (
<ConfirmTx {...args}>
<ModalListItem label="Transaction type" value="Bond" divider />
<ModalListItem label="Current bond" value="100 NYM" divider />
<ModalListItem label="Additional bond" value="50 NYM" divider />
<ModalListItem label="Transaction type:" value="Bond" divider />
<ModalListItem label="Current bond:" value="100 NYM" divider />
<ModalListItem label="Additional bond:" value="50 NYM" divider />
</ConfirmTx>
);
@@ -36,7 +36,7 @@ export const CopyToClipboard = ({ text = '', iconButton }: { text?: string; icon
color: 'text.primary',
}}
>
{!copied ? <ContentCopy fontSize="small" /> : <Check color="success" />}
{!copied ? <ContentCopy sx={{ fontSize: 14 }} /> : <Check color="success" sx={{ fontSize: 14 }} />}
</IconButton>
</Tooltip>
);
@@ -168,8 +168,8 @@ export const DelegateModal: React.FC<{
onPrev={resetFeeState}
onConfirm={handleOk}
>
<ModalListItem label="Node identity key" value={identityKey} divider />
<ModalListItem label="Amount" value={`${amount} ${denom.toUpperCase()}`} divider />
<ModalListItem label="Node identity key:" value={identityKey} divider />
<ModalListItem label="Amount:" value={`${amount} ${denom.toUpperCase()}`} divider />
</ConfirmTx>
);
}
@@ -184,23 +184,24 @@ export const DelegateModal: React.FC<{
}
}}
header={header || 'Delegate'}
subHeader="Delegate to mixnode"
okLabel={buttonText || 'Delegate stake'}
okDisabled={!isValidated}
sx={sx}
backdropProps={backdropProps}
>
<IdentityKeyFormField
required
fullWidth
placeholder="Node identity key"
onChanged={handleIdentityKeyChanged}
initialValue={identityKey}
readOnly={Boolean(initialIdentityKey)}
textFieldProps={{
autoFocus: !initialIdentityKey,
}}
/>
<Box sx={{ mt: 2 }}>
<IdentityKeyFormField
required
fullWidth
placeholder="Node identity key"
onChanged={handleIdentityKeyChanged}
initialValue={identityKey}
readOnly={Boolean(initialIdentityKey)}
textFieldProps={{
autoFocus: !initialIdentityKey,
}}
/>
</Box>
<Typography
component="div"
textAlign="left"
@@ -230,29 +231,30 @@ export const DelegateModal: React.FC<{
{errorAmount}
</Typography>
<Box sx={{ mt: 3 }}>
<ModalListItem label="Account balance" value={accountBalance} divider />
<ModalListItem label="Account balance:" value={accountBalance?.toUpperCase()} divider strong />
</Box>
<ModalListItem label="Rewards payout interval" value={rewardInterval} hidden divider />
<ModalListItem label="Rewards payout interval:" value={rewardInterval} hidden divider />
<ModalListItem
label="Node profit margin"
label="Node profit margin:"
value={`${profitMarginPercentage ? `${profitMarginPercentage}%` : '-'}`}
hidden={profitMarginPercentage === undefined}
divider
/>
<ModalListItem
label="Node uptime"
label="Node avg. uptime:"
value={`${nodeUptimePercentage ? `${nodeUptimePercentage}%` : '-'}`}
hidden={nodeUptimePercentage === undefined}
divider
/>
<ModalListItem
label="Node est. reward per epoch"
label="Node est. reward per epoch:"
value={`${estimatedReward} ${denom.toUpperCase()}`}
hidden
divider
/>
<ModalListItem label="Est. fee for this transaction will be calculated in the next page" />
</SimpleModal>
);
};
@@ -1,19 +1,8 @@
import React from 'react';
import {
Box,
Button,
IconButton,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Stack,
Tooltip,
Typography,
} from '@mui/material';
import { MoreVertSharp } from '@mui/icons-material';
import React, { useState } from 'react';
import { Box, Button, Stack, Tooltip, Typography } from '@mui/material';
import { DelegationEventKind } from '@nymproject/types';
import { Delegate, Undelegate } from '../../svg-icons';
import { ActionsMenu, ActionsMenuItem } from '../ActionsMenu';
import { DelegateListItemPending } from './types';
export type DelegationListItemActions = 'delegate' | 'undelegate' | 'redeem' | 'compound';
@@ -75,42 +64,20 @@ export const DelegationActions: React.FC<{
);
};
const DelegationActionsMenuItem = ({
title,
description,
onClick,
Icon,
disabled,
}: {
title: string;
description?: string;
onClick?: () => void;
Icon?: React.ReactNode;
disabled?: boolean;
}) => (
<MenuItem sx={{ p: 2 }} onClick={onClick} disabled={disabled}>
<ListItemIcon sx={{ color: 'text.primary' }}>{Icon}</ListItemIcon>
<ListItemText sx={{ color: 'text.primary' }} primary={title} secondary={description} />
</MenuItem>
);
export const DelegationsActionsMenu: React.FC<{
onActionClick?: (action: DelegationListItemActions) => void;
isPending?: DelegationEventKind;
disableRedeemingRewards?: boolean;
disableCompoundRewards?: boolean;
}> = ({ disableRedeemingRewards, disableCompoundRewards, onActionClick, isPending }) => {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const [isOpen, setIsOpen] = useState(false);
const handleClose = () => setAnchorEl(null);
const handleOpenMenu = () => setIsOpen(true);
const handleOnClose = () => setIsOpen(false);
const handleActionSelect = (action: DelegationListItemActions) => {
handleClose();
onActionClick?.(action);
handleOnClose();
};
if (isPending) {
@@ -126,37 +93,23 @@ export const DelegationsActionsMenu: React.FC<{
}
return (
<>
<IconButton onClick={handleClick}>
<MoreVertSharp />
</IconButton>
<Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
<DelegationActionsMenuItem
title="Delegate more"
Icon={<Delegate />}
onClick={() => handleActionSelect?.('delegate')}
/>
<DelegationActionsMenuItem
title="Undelegate"
Icon={<Undelegate />}
onClick={() => handleActionSelect?.('undelegate')}
disabled={false}
/>
<DelegationActionsMenuItem
title="Redeem"
description="Transfer your rewards to your balance"
Icon={<Typography sx={{ pl: 1 }}>R</Typography>}
onClick={() => handleActionSelect?.('redeem')}
disabled={disableRedeemingRewards}
/>
<DelegationActionsMenuItem
title="Compound"
description="Add your rewards to this delegation"
Icon={<Typography sx={{ pl: 1 }}>C</Typography>}
onClick={() => handleActionSelect?.('compound')}
disabled={disableCompoundRewards}
/>
</Menu>
</>
<ActionsMenu open={isOpen} onOpen={handleOpenMenu} onClose={handleOnClose}>
<ActionsMenuItem title="Delegate more" Icon={<Delegate />} onClick={() => handleActionSelect('delegate')} />
<ActionsMenuItem title="Undelegate" Icon={<Undelegate />} onClick={() => handleActionSelect('undelegate')} />
<ActionsMenuItem
title="Redeem"
description="Trasfer your rewards to your balance"
Icon={<Typography sx={{ pl: 1 }}>R</Typography>}
onClick={() => handleActionSelect('redeem')}
disabled={disableRedeemingRewards}
/>
<ActionsMenuItem
title="Compound"
description="Add your rewards to this delegation"
Icon={<Typography sx={{ pl: 1 }}>C</Typography>}
onClick={() => handleActionSelect('compound')}
disabled={disableCompoundRewards}
/>
</ActionsMenu>
);
};
@@ -29,9 +29,6 @@ const transactionForDarkTheme = {
url: 'https://sandbox-blocks.nymtech.net/transactions/11ED7B9E21534A9421834F52FED5103DC6E982949C06335F5E12EFC71DAF0CFO',
hash: '11ED7B9E21534A9421834F52FED5103DC6E982949C06335F5E12EFC71DAF0CF0',
};
const balance = '104 NYMT';
const balanceVested = '12 NYMT';
const recipient = 'nymt1923pujepxfnv8dqyxqrl078s4ysf3xn2p7z2xa';
const Content: React.FC<{ children: React.ReactElement<any, any>; handleClick: () => void }> = ({
children,
@@ -78,8 +75,6 @@ export const DelegateSuccess = () => {
status="success"
action="delegate"
message="You delegated 5 NYM"
recipient={recipient}
balance={balance}
transactions={theme.palette.mode === 'light' ? [transaction] : [transactionForDarkTheme]}
{...storybookStyles(theme)}
/>
@@ -99,8 +94,6 @@ export const UndelegateSuccess = () => {
status="success"
action="undelegate"
message="You undelegated 5 NYM"
recipient={recipient}
balance={balance}
transactions={theme.palette.mode === 'light' ? [transaction] : [transactionForDarkTheme]}
{...storybookStyles(theme)}
/>
@@ -120,8 +113,6 @@ export const RedeemSuccess = () => {
status="success"
action="redeem"
message="42 NYM"
recipient={recipient}
balance={balance}
transactions={
theme.palette.mode === 'light'
? [transaction, transaction]
@@ -145,9 +136,6 @@ export const RedeemWithVestedSuccess = () => {
status="success"
action="redeem"
message="42 NYM"
recipient={recipient}
balance={balance}
balanceVested={balanceVested}
transactions={
theme.palette.mode === 'light'
? [transaction, transaction]
@@ -171,8 +159,6 @@ export const RedeemAllSuccess = () => {
status="success"
action="redeem-all"
message="42 NYM"
recipient={recipient}
balance={balance}
transactions={
theme.palette.mode === 'light'
? [transaction, transaction]
@@ -196,8 +182,6 @@ export const Error = () => {
status="error"
action="redeem-all"
message="Minim esse veniam Lorem id velit Lorem eu eu est. Excepteur labore sunt do proident proident sint aliquip consequat Lorem sint non nulla ad excepteur."
recipient={recipient}
balance={balance}
transactions={theme.palette.mode === 'light' ? [transaction] : [transactionForDarkTheme]}
{...storybookStyles(theme)}
/>
@@ -1,9 +1,10 @@
import React from 'react';
import { Box, Button, Modal, Typography, SxProps } from '@mui/material';
import { Box, Button, Modal, Typography, SxProps, Stack } from '@mui/material';
import { Link } from '@nymproject/react/link/Link';
import { Console } from 'src/utils/console';
import { modalStyle } from '../Modals/styles';
import { LoadingModal } from '../Modals/LoadingModal';
import { ConfirmationModal } from '../Modals/ConfirmationModal';
export type ActionType = 'delegate' | 'undelegate' | 'redeem' | 'redeem-all' | 'compound';
@@ -15,9 +16,9 @@ const actionToHeader = (action: ActionType): string => {
case 'redeem-all':
return 'All rewards redeemed successfully';
case 'delegate':
return 'Delegation complete';
return 'Delegation successful';
case 'undelegate':
return 'Undelegation complete';
return 'Undelegation successful';
case 'compound':
return 'Rewards compounded successfully';
default:
@@ -29,9 +30,6 @@ export type DelegationModalProps = {
status: 'loading' | 'success' | 'error';
action: ActionType;
message?: string;
recipient?: string;
balance?: string;
balanceVested?: string;
transactions?: {
url: string;
hash: string;
@@ -45,20 +43,7 @@ export const DelegationModal: React.FC<
sx?: SxProps;
backdropProps?: object;
}
> = ({
status,
action,
message,
recipient,
balance,
balanceVested,
transactions,
open,
onClose,
children,
sx,
backdropProps,
}) => {
> = ({ status, action, message, transactions, open, onClose, children, sx, backdropProps }) => {
if (status === 'loading') return <LoadingModal sx={sx} backdropProps={backdropProps} />;
if (status === 'error') {
@@ -81,54 +66,28 @@ export const DelegationModal: React.FC<
}
transactions?.map((transaction) => Console.log('action', action, 'status', status, 'key', transaction.hash));
return (
<Modal open={open} onClose={onClose} BackdropProps={backdropProps}>
<Box sx={{ ...modalStyle, ...sx }} textAlign="center">
<Typography color={(theme) => theme.palette.success.main} mb={1}>
{actionToHeader(action)}
</Typography>
<Typography mb={3} color="text.primary">
{message}
</Typography>
{recipient && (
<Typography mb={1} fontSize="small" color={(theme) => theme.palette.text.secondary}>
Recipient: {recipient}
</Typography>
return (
<ConfirmationModal
open={open}
onConfirm={onClose || (() => {})}
title={actionToHeader(action)}
confirmButton="Done"
>
<Stack alignItems="center" spacing={2} mb={0}>
{message && <Typography>{message}</Typography>}
{transactions?.length === 1 && (
<Link href={transactions[0].url} target="_blank" sx={{ ml: 1 }} text="View on blockchain" noIcon />
)}
{balanceVested ? (
<>
<Typography mb={1} fontSize="small" color={(theme) => theme.palette.text.secondary}>
Your current balance: {balance?.toUpperCase()}
</Typography>
<Typography mb={1} fontSize="small" color={(theme) => theme.palette.text.secondary}>
({balanceVested.toUpperCase()} is unlocked in your vesting account)
</Typography>
</>
) : (
<Typography mb={1} fontSize="small" color={(theme) => theme.palette.text.secondary}>
Your current balance: {balance?.toUpperCase()}
</Typography>
)}
{transactions && (
<Typography mb={1} fontSize="small" color={(theme) => theme.palette.text.secondary}>
Check the transaction {transactions.length > 1 ? 'hashes' : 'hash'}:
{transactions.map((transaction) => (
<Link
key={transaction.hash}
href={transaction.url}
target="_blank"
sx={{ ml: 1 }}
text={transaction.hash.slice(0, 6)}
/>
{transactions && transactions.length > 1 && (
<Stack alignItems="center" spacing={1}>
<Typography>View the transactions on blockchain:</Typography>
{transactions.map(({ url, hash }) => (
<Link href={url} target="_blank" sx={{ ml: 1 }} text={hash.slice(0, 6)} key={hash} noIcon />
))}
</Typography>
</Stack>
)}
{children}
<Button variant="contained" sx={{ mt: 3 }} size="large" onClick={onClose}>
Finish
</Button>
</Box>
</Modal>
</Stack>
</ConfirmationModal>
);
};
@@ -55,7 +55,7 @@ export const UndelegateModal: React.FC<{
/>
<Box sx={{ mt: 3 }}>
<ModalListItem label="Delegation amount" value={`${amount} ${currency}`} divider />
<ModalListItem label="Delegation amount:" value={`${amount} ${currency}`} divider />
</Box>
<Typography mb={5} fontSize="smaller" sx={{ color: 'text.primary' }}>
+13
View File
@@ -0,0 +1,13 @@
import React from 'react';
import { Stack, Typography } from '@mui/material';
import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard';
import { splice } from 'src/utils';
export const IdentityKey = ({ identityKey }: { identityKey: string }) => (
<Stack direction="row">
<Typography variant="body2" component="span" fontWeight={400} sx={{ mr: 1, color: 'text.primary' }}>
{splice(6, identityKey)}
</Typography>
<CopyToClipboard value={identityKey} sx={{ fontSize: 18 }} />
</Stack>
);
@@ -41,7 +41,7 @@ export const ConfirmationModal = ({
backdropProps,
}: ConfirmationModalProps) => {
const Title = (
<DialogTitle id="responsive-dialog-title" sx={{ py: 3, pb: 2, fontWeight: 600 }} color="text.primary">
<DialogTitle id="responsive-dialog-title" sx={{ pb: 2 }}>
{title}
{subTitle &&
(typeof subTitle === 'string' ? (
@@ -0,0 +1,26 @@
import React from 'react';
import { Box, Button, Modal, SxProps, Typography } from '@mui/material';
import { modalStyle } from './styles';
export const ErrorModal: React.FC<{
open: boolean;
message?: string;
sx?: SxProps;
backdropProps?: object;
onClose: () => void;
}> = ({ children, open, message, sx, backdropProps, onClose }) => (
<Modal open={open} onClose={onClose} BackdropProps={backdropProps}>
<Box sx={{ ...modalStyle, ...sx }} textAlign="center">
<Typography color={(theme) => theme.palette.error.main} mb={1}>
Oh no! Something went wrong...
</Typography>
<Typography my={5} color="text.primary">
{message}
</Typography>
{children}
<Button variant="contained" onClick={onClose}>
Close
</Button>
</Box>
</Modal>
);
@@ -2,16 +2,20 @@ import React from 'react';
import { FeeDetails } from '@nymproject/types';
import { CircularProgress } from '@mui/material';
import { ModalListItem } from './ModalListItem';
import { ModalDivider } from './ModalDivider';
type TFeeProps = { fee?: FeeDetails; isLoading: boolean; error?: string };
type TFeeProps = { fee?: FeeDetails; isLoading: boolean; error?: string; divider?: boolean };
const getValue = ({ fee, isLoading, error }: TFeeProps) => {
if (isLoading) return <CircularProgress size={15} />;
if (error && !isLoading) return 'n/a';
if (fee) return `${fee.amount?.amount} ${fee.amount?.denom}`;
if (fee) return `${fee.amount?.amount} ${fee.amount?.denom.toUpperCase()}`;
return '-';
};
export const ModalFee = ({ fee, isLoading, error }: TFeeProps) => (
<ModalListItem label="Estimated fee for this operation" value={getValue({ fee, isLoading, error })} />
export const ModalFee = ({ fee, isLoading, error, divider }: TFeeProps) => (
<>
<ModalListItem label="Fee for this operation:" value={getValue({ fee, isLoading, error })} />
{divider && <ModalDivider />}
</>
);
@@ -7,20 +7,18 @@ export const ModalListItem: React.FC<{
divider?: boolean;
hidden?: boolean;
strong?: boolean;
value: React.ReactNode;
value?: React.ReactNode;
}> = ({ label, value, hidden, divider, strong }) => (
<Box sx={{ display: hidden ? 'none' : 'block' }}>
<Stack direction="row" justifyContent="space-between">
<Typography fontSize="smaller" fontWeight={strong ? 600 : undefined} sx={{ color: 'text.primary' }}>
{label}:
</Typography>
<Typography
fontSize="smaller"
fontWeight={strong ? 600 : undefined}
sx={{ color: 'text.primary', textTransform: 'uppercase' }}
>
{value}
{label}
</Typography>
{value && (
<Typography fontSize="smaller" fontWeight={strong ? 600 : undefined} sx={{ color: 'text.primary' }}>
{value}
</Typography>
)}
</Stack>
{divider && <ModalDivider />}
</Box>
@@ -14,7 +14,7 @@ export const SimpleModal: React.FC<{
onClose?: () => void;
onOk?: () => Promise<void>;
onBack?: () => void;
header: string;
header: string | React.ReactNode;
subHeader?: string;
okLabel: string;
okDisabled?: boolean;
@@ -41,9 +41,13 @@ export const SimpleModal: React.FC<{
<Box sx={{ ...modalStyle, ...sx }}>
{displayErrorIcon && <ErrorOutline color="error" sx={{ mb: 3 }} />}
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Typography fontSize={20} fontWeight={600} sx={{ color: 'text.primary', ...headerStyles }}>
{header}
</Typography>
{typeof header === 'string' ? (
<Typography fontSize={20} fontWeight={600} sx={{ color: 'text.primary', ...headerStyles }}>
{header}
</Typography>
) : (
header
)}
{!hideCloseIcon && <CloseIcon onClick={onClose} cursor="pointer" />}
</Stack>
{subHeader && (
+34 -15
View File
@@ -2,8 +2,8 @@ import React, { useState, useContext } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { List, ListItem, ListItemIcon, ListItemText } from '@mui/material';
import { AccountBalanceWalletOutlined, ArrowBack, ArrowForward, Description, Settings } from '@mui/icons-material';
import { AppContext } from '../context';
import { Bond, Delegate, Unbond } from '../svg-icons';
import { AppContext } from '../context/main';
import { Delegate, Bonding } from '../svg-icons';
export const Nav = () => {
const location = useLocation();
@@ -29,16 +29,10 @@ export const Nav = () => {
onClick: handleShowReceiveModal,
},
{
label: 'Bond',
route: '/bond',
Icon: Bond,
onClick: () => navigate('/bond'),
},
{
label: 'Unbond',
route: '/unbond',
Icon: Unbond,
onClick: () => navigate('/unbond'),
label: 'Bonding',
route: '/bonding',
Icon: Bonding,
onClick: () => navigate('/bonding'),
},
{
label: 'Delegation',
@@ -68,9 +62,16 @@ export const Nav = () => {
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
marginLeft: 12,
marginRight: 12,
}}
>
<List disablePadding>
<List
disablePadding
sx={{
width: '100%',
}}
>
{routesSchema
.filter(({ mode }) => {
if (!mode) {
@@ -86,17 +87,35 @@ export const Nav = () => {
}
})
.map(({ Icon, onClick, label, route }) => (
<ListItem disableGutters key={label} onClick={onClick} sx={{ cursor: 'pointer' }}>
<ListItem
disableGutters
key={label}
onClick={onClick}
sx={{
cursor: 'pointer',
py: 2,
paddingLeft: 3.5,
borderRadius: 1,
'&:hover': { backgroundColor: (theme) => theme.palette.nym.nymWallet.hover.background },
}}
>
<ListItemIcon
sx={{
height: '20px',
minWidth: 30,
color: location.pathname === route ? 'primary.main' : 'text.primary',
}}
>
<Icon sx={{ fontSize: 20 }} />
<Icon
sx={{
fontSize: 20,
}}
/>
</ListItemIcon>
<ListItemText
sx={{
height: '20px',
margin: 0,
color: location.pathname === route ? 'primary.main' : 'text.primary',
'& .MuiListItemText-primary': {
fontSize: 14,
+1 -1
View File
@@ -26,7 +26,7 @@ export const NymCard: React.FC<{
subheader={subheader}
data-testid={dataTestid || title}
subheaderTypographyProps={{ variant: 'subtitle1' }}
action={<Box sx={{ mt: 1, mr: 1 }}>{Action}</Box>}
action={Action}
/>
{noPadding ? (
<CardContentNoPadding>{children}</CardContentNoPadding>
@@ -1,5 +1,4 @@
import React, { useEffect } from 'react';
import { Stack, Typography } from '@mui/material';
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
import { CurrencyDenom, FeeDetails } from '@nymproject/types';
import { simulateCompoundDelgatorReward, simulateVestingCompoundDelgatorReward } from 'src/requests';
@@ -7,6 +6,7 @@ import { useGetFee } from 'src/hooks/useGetFee';
import { SimpleModal } from '../Modals/SimpleModal';
import { ModalFee } from '../Modals/ModalFee';
import { FeeWarning } from '../FeeWarning';
import { ModalListItem } from '../Modals/ModalListItem';
export const CompoundModal: React.FC<{
open: boolean;
@@ -42,20 +42,13 @@ export const CompoundModal: React.FC<{
subHeader="Compound rewards from delegations"
okLabel="Compound rewards"
>
{identityKey && <IdentityKeyFormField readOnly fullWidth initialValue={identityKey} showTickOnValid={false} />}
<Stack direction="row" justifyContent="space-between" mb={4} mt={identityKey && 4}>
<Typography>Rewards amount:</Typography>
<Typography>
{amount} {denom.toUpperCase()}
</Typography>
</Stack>
<Typography mb={5} fontSize="smaller">
Rewards will be transferred to account you are logged in with now
</Typography>
{identityKey && (
<IdentityKeyFormField readOnly fullWidth initialValue={identityKey} showTickOnValid={false} sx={{ mb: 2 }} />
)}
<ModalListItem label="Rewards amount" value={` ${amount} ${denom.toUpperCase()}`} divider />
{fee && <FeeWarning amount={amount} fee={fee} />}
<ModalFee fee={fee} isLoading={isFeeLoading} error={feeError} />
<ModalFee fee={fee} isLoading={isFeeLoading} error={feeError} divider />
<ModalListItem label="Rewards will be added to this delegation" value="" divider />
</SimpleModal>
);
};
@@ -1,12 +1,13 @@
import React, { useEffect } from 'react';
import { Stack, Typography, SxProps } from '@mui/material';
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
import { CurrencyDenom, FeeDetails } from '@nymproject/types';
import { SxProps } from '@mui/material';
import { useGetFee } from 'src/hooks/useGetFee';
import { simulateClaimDelgatorReward, simulateVestingClaimDelgatorReward } from 'src/requests';
import { ModalFee } from '../Modals/ModalFee';
import { SimpleModal } from '../Modals/SimpleModal';
import { FeeWarning } from '../FeeWarning';
import { ModalListItem } from '../Modals/ModalListItem';
export const RedeemModal: React.FC<{
open: boolean;
@@ -47,20 +48,13 @@ export const RedeemModal: React.FC<{
sx={sx}
backdropProps={backdropProps}
>
{identityKey && <IdentityKeyFormField readOnly fullWidth initialValue={identityKey} showTickOnValid={false} />}
<Stack direction="row" justifyContent="space-between" mb={4} mt={identityKey && 4}>
<Typography sx={{ color: 'text.primary' }}>Rewards amount:</Typography>
<Typography sx={{ color: 'text.primary' }}>
{amount} {denom.toUpperCase()}
</Typography>
</Stack>
<Typography mb={5} fontSize="smaller" sx={{ color: 'text.primary' }}>
Rewards will be transferred to account you are logged in with now
</Typography>
{identityKey && (
<IdentityKeyFormField readOnly fullWidth initialValue={identityKey} showTickOnValid={false} sx={{ mb: 2 }} />
)}
<ModalListItem label="Rewards amount" value={` ${amount} ${denom.toUpperCase()}`} divider />
{fee && <FeeWarning amount={amount} fee={fee} />}
<ModalFee fee={fee} isLoading={isFeeLoading} error={feeError} />
<ModalFee fee={fee} isLoading={isFeeLoading} error={feeError} divider />
<ModalListItem label="Rewards will be transferred to account you are logged in with now" value="" divider />
</SimpleModal>
);
};
@@ -30,6 +30,7 @@ export const SendInput = () => {
<SendInputModal
toAddress=""
fromAddress="nymt1w8qp7zsxggvtxhpqpt6e329j42wtv07dm5ts8u"
denom="nym"
onNext={() => {}}
onClose={() => {}}
onAddressChange={() => {}}
@@ -1,9 +1,9 @@
import React from 'react';
import { Stack, SxProps } from '@mui/material';
import { CurrencyDenom } from '@nymproject/types';
import { FeeDetails, DecCoin } from '@nymproject/types';
import { FeeDetails, DecCoin, CurrencyDenom } from '@nymproject/types';
import { SimpleModal } from '../Modals/SimpleModal';
import { ModalListItem } from '../Modals/ModalListItem';
import { ModalFee } from '../Modals/ModalFee';
export const SendDetailsModal = ({
amount,
@@ -39,14 +39,10 @@ export const SendDetailsModal = ({
backdropProps={backdropProps}
>
<Stack gap={0.5} sx={{ mt: 4 }}>
<ModalListItem label="From" value={fromAddress} divider />
<ModalListItem label="To" value={toAddress} divider />
<ModalListItem label="Amount" value={`${amount?.amount} ${denom.toUpperCase()}`} divider />
<ModalListItem
label="Fee for this transaction"
value={!fee ? 'n/a' : `${fee.amount?.amount} ${fee.amount?.denom}`}
divider
/>
<ModalListItem label="From:" value={fromAddress} divider />
<ModalListItem label="To:" value={toAddress} divider />
<ModalListItem label="Amount:" value={`${amount?.amount} ${denom.toUpperCase()}`} divider />
<ModalFee fee={fee} divider isLoading={false} />
</Stack>
</SimpleModal>
);
@@ -77,8 +77,8 @@ export const SendInputModal = ({
</Typography>
</Stack>
<Stack gap={0.5} sx={{ mt: 2 }}>
<ModalListItem label="Account balance" value={balance} divider strong />
<ModalListItem label="Your address" value={fromAddress} divider />
<ModalListItem label="Account balance:" value={balance?.toUpperCase()} divider strong />
<ModalListItem label="Your address:" value={fromAddress} divider />
<Typography fontSize="smaller" sx={{ color: 'text.primary' }}>
Est. fee for this transaction will be show on the next page
</Typography>
+35
View File
@@ -0,0 +1,35 @@
import React from 'react';
import { Tab, Tabs as MuiTabs } from '@mui/material';
export const Tabs: React.FC<{
tabs: string[];
selectedTab: number;
disabled?: boolean;
onChange?: (event: React.SyntheticEvent, tab: number) => void;
disableActiveTabHighlight?: boolean;
}> = ({ tabs, selectedTab, disabled, disableActiveTabHighlight, onChange }) => (
<MuiTabs
value={selectedTab}
onChange={onChange}
sx={{
bgcolor: (theme) => theme.palette.nym.nymWallet.background.grey,
borderTop: '1px solid',
borderBottom: '1px solid',
borderColor: (theme) => theme.palette.nym.nymWallet.background.greyStroke,
}}
textColor="inherit"
TabIndicatorProps={
disableActiveTabHighlight
? {
style: {
opacity: 0,
},
}
: {}
}
>
{tabs.map((tabName) => (
<Tab key={tabName} label={tabName} sx={{ textTransform: 'capitalize' }} disabled={disabled} />
))}
</MuiTabs>
);
@@ -14,11 +14,13 @@ export const TokenPoolSelector: React.FC<{ disabled: boolean; onSelect: (pool: T
clientDetails,
} = useContext(AppContext);
const fetchBalances = async () => {
await fetchBalance();
await fetchTokenAllocation();
};
useEffect(() => {
(async () => {
await fetchBalance();
await fetchTokenAllocation();
})();
fetchBalances();
}, []);
useEffect(() => {
+358
View File
@@ -0,0 +1,358 @@
import { FeeDetails, DecCoin, MixnodeStatus, TransactionExecuteResult } from '@nymproject/types';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { isGateway, isMixnode, Network, TBondGatewayArgs, TBondMixNodeArgs } from 'src/types';
import { Console } from 'src/utils/console';
import {
bondGateway as bondGatewayRequest,
bondMixNode as bondMixNodeRequest,
claimOperatorReward,
compoundOperatorReward,
unbondGateway as unbondGatewayRequest,
unbondMixNode as unbondMixnodeRequest,
vestingBondGateway,
vestingBondMixNode,
vestingUnbondGateway,
vestingUnbondMixnode,
updateMixnode as updateMixnodeRequest,
vestingUpdateMixnode as updateMixnodeVestingRequest,
getNodeDescription as getNodeDescriptioRequest,
getGatewayBondDetails,
getMixnodeBondDetails,
getMixnodeStatus,
getOperatorRewards,
getMixnodeStakeSaturation,
getNumberOfMixnodeDelegators,
vestingClaimOperatorReward,
vestingCompoundOperatorReward,
} from '../requests';
import { useCheckOwnership } from '../hooks/useCheckOwnership';
import { AppContext } from './main';
const bonded: TBondedMixnode = {
name: 'Monster node',
identityKey: 'B2Xx4haarLWMajX8w259oHjtRZsC7nHwagbWrJNiA3QC',
bond: { denom: 'nym', amount: '1234' },
delegators: 123,
operatorRewards: { denom: 'nym', amount: '12' },
profitMargin: 10,
stake: { denom: 'nym', amount: '99' },
stakeSaturation: 99,
status: 'active',
};
// TODO add relevant data
export type TBondedMixnode = {
name?: string;
identityKey: string;
stake: DecCoin;
bond: DecCoin;
stakeSaturation: number;
profitMargin: number;
operatorRewards: DecCoin;
delegators: number;
status: MixnodeStatus;
proxy?: string;
};
// TODO add relevant data
export interface TBondedGateway {
name: string;
identityKey: string;
ip: string;
bond: DecCoin;
location?: string; // TODO not yet available, only available in Network Explorer API
proxy?: string;
}
export type TokenPool = 'locked' | 'balance';
export type TBondingContext = {
isLoading: boolean;
error?: string;
bondedNode?: TBondedMixnode | TBondedGateway;
refresh: () => Promise<void>;
bondMixnode: (data: TBondMixNodeArgs, tokenPool: TokenPool) => Promise<TransactionExecuteResult | undefined>;
bondGateway: (data: TBondGatewayArgs, tokenPool: TokenPool) => Promise<TransactionExecuteResult | undefined>;
unbond: (fee?: FeeDetails) => Promise<TransactionExecuteResult | undefined>;
redeemRewards: (fee?: FeeDetails) => Promise<TransactionExecuteResult | undefined>;
compoundRewards: (fee?: FeeDetails) => Promise<TransactionExecuteResult | undefined>;
updateMixnode: (pm: number, fee?: FeeDetails) => Promise<TransactionExecuteResult | undefined>;
checkOwnership: () => Promise<void>;
};
const calculateStake = (pledge: DecCoin, delegations: DecCoin) => {
const total = Number(pledge.amount) + Number(delegations.amount);
return { amount: total.toString(), denom: pledge.denom };
};
export const BondingContext = createContext<TBondingContext>({
isLoading: true,
refresh: async () => undefined,
bondMixnode: async () => {
throw new Error('Not implemented');
},
bondGateway: async () => {
throw new Error('Not implemented');
},
unbond: async () => {
throw new Error('Not implemented');
},
redeemRewards: async () => {
throw new Error('Not implemented');
},
compoundRewards: async () => {
throw new Error('Not implemented');
},
updateMixnode: async () => {
throw new Error('Not implemented');
},
checkOwnership(): Promise<void> {
throw new Error('Not implemented');
},
});
export const BondingContextProvider = ({ children }: { children?: React.ReactNode }): JSX.Element => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string>();
const [bondedNode, setBondedNode] = useState<TBondedMixnode | TBondedGateway>();
const { userBalance, clientDetails } = useContext(AppContext);
const { ownership, isLoading: isOwnershipLoading, checkOwnership } = useCheckOwnership();
const isVesting = Boolean(ownership.vestingPledge);
const resetState = () => {
setError(undefined);
setBondedNode(undefined);
};
const getAdditionalMixnodeDetails = async (identityKey: string) => {
const additionalDetails: { status: MixnodeStatus; stakeSaturation: number; numberOfDelegators: number } = {
status: 'not_found',
stakeSaturation: 0,
numberOfDelegators: 0,
};
try {
const statusResponse = await getMixnodeStatus(identityKey);
additionalDetails.status = statusResponse.status;
} catch (e) {
Console.log(e);
}
try {
const stakeSaturationResponse = await getMixnodeStakeSaturation(identityKey);
additionalDetails.stakeSaturation = Math.round(stakeSaturationResponse.saturation * 100);
} catch (e) {
Console.log(e);
}
try {
const numberOfDelegators = await getNumberOfMixnodeDelegators(identityKey);
additionalDetails.numberOfDelegators = numberOfDelegators;
} catch (e) {
Console.log(e);
}
return additionalDetails;
};
const getNodeDescription = async (host: string, port: number) => {
try {
const nodeDescription = await getNodeDescriptioRequest(host, port);
return nodeDescription;
} catch (e) {
Console.log(e);
}
return undefined;
};
const refresh = useCallback(async () => {
setIsLoading(true);
if (ownership.hasOwnership && ownership.nodeType === 'mixnode' && clientDetails) {
try {
const data = await getMixnodeBondDetails();
const operatorRewards = await getOperatorRewards(clientDetails?.client_address);
if (data) {
const { status, stakeSaturation, numberOfDelegators } = await getAdditionalMixnodeDetails(
data.mix_node.identity_key,
);
const nodeDescription = await getNodeDescription(data.mix_node.host, data.mix_node.http_api_port);
setBondedNode({
name: nodeDescription?.name,
identityKey: data.mix_node.identity_key,
ip: '',
stake: calculateStake(data.pledge_amount, data.total_delegation),
bond: data.pledge_amount,
profitMargin: data.mix_node.profit_margin_percent,
nodeRewards: data.accumulated_rewards,
delegators: numberOfDelegators,
proxy: data.proxy,
operatorRewards,
status,
stakeSaturation,
} as TBondedMixnode);
}
} catch (e: any) {
setError(`While fetching current bond state, an error occurred: ${e}`);
}
}
if (ownership.hasOwnership && ownership.nodeType === 'gateway') {
try {
const data = await getGatewayBondDetails();
if (data) {
const nodeDescription = await getNodeDescription(data.gateway.host, data.gateway.clients_port);
setBondedNode({
name: nodeDescription?.name,
identityKey: data.gateway.identity_key,
ip: data.gateway.host,
location: data.gateway.location,
bond: data.pledge_amount,
delegators: bonded.delegators,
proxy: data.proxy,
} as TBondedGateway);
}
} catch (e: any) {
setError(`While fetching current bond state, an error occurred: ${e}`);
}
}
if (!ownership.hasOwnership) {
resetState();
}
setIsLoading(false);
}, [ownership]);
useEffect(() => {
refresh();
}, [ownership, refresh]);
const bondMixnode = async (data: TBondMixNodeArgs, tokenPool: TokenPool) => {
let tx: TransactionExecuteResult | undefined;
setIsLoading(true);
try {
if (tokenPool === 'balance') {
tx = await bondMixNodeRequest(data);
await userBalance.fetchBalance();
}
if (tokenPool === 'locked') {
tx = await vestingBondMixNode(data);
await userBalance.fetchTokenAllocation();
}
return tx;
} catch (e: any) {
setError(`an error occurred: ${e}`);
} finally {
setIsLoading(false);
}
return undefined;
};
const bondGateway = async (data: TBondGatewayArgs, tokenPool: TokenPool) => {
let tx: TransactionExecuteResult | undefined;
setIsLoading(true);
try {
if (tokenPool === 'balance') {
tx = await bondGatewayRequest(data);
await userBalance.fetchBalance();
}
if (tokenPool === 'locked') {
tx = await vestingBondGateway(data);
await userBalance.fetchTokenAllocation();
}
return tx;
} catch (e: any) {
setError(`an error occurred: ${e}`);
} finally {
setIsLoading(false);
}
return undefined;
};
const unbond = async (fee?: FeeDetails) => {
let tx;
setIsLoading(true);
try {
if (bondedNode && isMixnode(bondedNode) && bondedNode.proxy) tx = await vestingUnbondMixnode(fee?.fee);
if (bondedNode && isMixnode(bondedNode) && !bondedNode.proxy) tx = await unbondMixnodeRequest(fee?.fee);
if (bondedNode && isGateway(bondedNode) && bondedNode.proxy) tx = await vestingUnbondGateway(fee?.fee);
if (bondedNode && isGateway(bondedNode) && !bondedNode.proxy) tx = await unbondGatewayRequest(fee?.fee);
} catch (e) {
setError(`an error occurred: ${e as string}`);
} finally {
setIsLoading(false);
}
return tx;
};
const updateMixnode = async (pm: number, fee?: FeeDetails) => {
let tx;
setIsLoading(true);
try {
if (bondedNode?.proxy) tx = await updateMixnodeVestingRequest(pm, fee?.fee);
else tx = await updateMixnodeRequest(pm, fee?.fee);
} catch (e: any) {
setError(`an error occurred: ${e}`);
} finally {
setIsLoading(false);
}
return tx;
};
const redeemRewards = async (fee?: FeeDetails) => {
let tx;
setIsLoading(true);
try {
if (bondedNode?.proxy) tx = await vestingClaimOperatorReward(fee?.fee);
else tx = await claimOperatorReward(fee?.fee);
} catch (e: any) {
setError(`an error occurred: ${e}`);
} finally {
setIsLoading(false);
}
return tx;
};
const compoundRewards = async (fee?: FeeDetails) => {
let tx;
setIsLoading(true);
try {
if (bondedNode?.proxy) tx = await vestingCompoundOperatorReward(fee?.fee);
else tx = await compoundOperatorReward(fee?.fee);
} catch (e: any) {
setError(`an error occurred: ${e}`);
} finally {
setIsLoading(false);
}
return tx;
};
const bondMore = async (_signature: string, _additionalBond: DecCoin) =>
// TODO to implement
undefined;
const memoizedValue = useMemo(
() => ({
isLoading: isLoading || isOwnershipLoading,
error,
bondMixnode,
bondedNode,
bondGateway,
unbond,
updateMixnode,
refresh,
redeemRewards,
compoundRewards,
bondMore,
checkOwnership,
}),
[isLoading, isOwnershipLoading, error, bondedNode, isVesting],
);
return <BondingContext.Provider value={memoizedValue}>{children}</BondingContext.Provider>;
};
export const useBondingContext = () => useContext<TBondingContext>(BondingContext);
+1
View File
@@ -1,3 +1,4 @@
export * from './main';
export * from './auth';
export * from './accounts';
export * from './bonding';
+2 -9
View File
@@ -33,7 +33,6 @@ type TLoginType = 'mnemonic' | 'password';
export type TAppContext = {
mode: 'light' | 'dark';
handleSwitchMode: () => void;
appEnv?: AppEnv;
appVersion?: string;
clientDetails?: Account;
@@ -47,10 +46,10 @@ export type TAppContext = {
isAdminAddress: boolean;
error?: string;
loginType?: TLoginType;
showSettings: boolean;
showSendModal: boolean;
showReceiveModal: boolean;
handleShowSettings: () => void;
onAccountChange: ({ accountId, password }: { accountId: string; password: string }) => void;
handleSwitchMode: () => void;
handleShowSendModal: () => void;
handleShowReceiveModal: () => void;
setIsLoading: (isLoading: boolean) => void;
@@ -62,7 +61,6 @@ export type TAppContext = {
handleShowTerminal: () => void;
signInWithPassword: (password: string) => void;
logOut: () => void;
onAccountChange: ({ accountId, password }: { accountId: string; password: string }) => void;
};
export const AppContext = createContext({} as TAppContext);
@@ -81,7 +79,6 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const [error, setError] = useState<string>();
const [appVersion, setAppVersion] = useState<string>();
const [isAdminAddress, setIsAdminAddress] = useState<boolean>(false);
const [showSettings, setShowSettings] = useState(false);
const [showSendModal, setShowSendModal] = useState(false);
const [showReceiveModal, setShowReceiveModal] = useState(false);
@@ -235,7 +232,6 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const handleShowAdmin = () => setShowAdmin((show) => !show);
const handleShowTerminal = () => setShowTerminal((show) => !show);
const switchNetwork = (_network: Network) => setNetwork(_network);
const handleShowSettings = () => setShowSettings((show) => !show);
const handleShowSendModal = () => setShowSendModal((show) => !show);
const handleShowReceiveModal = () => setShowReceiveModal((show) => !show);
const handleSwitchMode = () =>
@@ -259,7 +255,6 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
userBalance,
showAdmin,
showTerminal,
showSettings,
network,
loginType,
setIsLoading,
@@ -272,7 +267,6 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
logIn,
logOut,
onAccountChange,
handleShowSettings,
showSendModal,
showReceiveModal,
handleShowSendModal,
@@ -294,7 +288,6 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
network,
storedAccounts,
showTerminal,
showSettings,
showSendModal,
showReceiveModal,
],
+181
View File
@@ -0,0 +1,181 @@
import { FeeDetails, DecCoin, TransactionExecuteResult } from '@nymproject/types';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import type { Network } from 'src/types';
import { TBondedGateway, TBondedMixnode, BondingContext } from '../bonding';
import { mockSleep } from './utils';
const SLEEP_MS = 1000;
const bondedMixnodeMock: TBondedMixnode = {
name: 'Monster node',
identityKey: '7mjM2fYbtN6kxMwp1TrmQ4VwPks3URR5pBgWPWhzT98F',
stake: { denom: 'nym', amount: '1234' },
bond: { denom: 'nym', amount: '1234' },
stakeSaturation: 95,
profitMargin: 15,
operatorRewards: { denom: 'nym', amount: '1234' },
delegators: 5423,
status: 'active',
};
const bondedGatewayMock: TBondedGateway = {
name: 'Monster node',
identityKey: 'WayM2fYbtN6kxMwp1TrmQ4VwPks3URR5pBgWPWhzT98F',
ip: '112.43.234.57',
bond: { denom: 'nym', amount: '1234' },
};
const TxResultMock: TransactionExecuteResult = {
logs_json: '',
data_json: '',
transaction_hash: '55303CD4B91FAC4C2715E40EBB52BB3B92829D9431B3A279D37B5CC58432E354',
gas_info: {
gas_wanted: { gas_units: BigInt(1) },
gas_used: { gas_units: BigInt(1) },
},
fee: { amount: '1', denom: 'nym' },
};
const feeMock: FeeDetails = {
amount: { denom: 'nym', amount: '1' },
fee: { Auto: 1 },
};
export const MockBondingContextProvider = ({
network,
children,
}: {
network?: Network;
children?: React.ReactNode;
}): JSX.Element => {
const [isLoading, setIsLoading] = useState(true);
const [feeLoading, setFeeLoading] = useState(false);
const [fee, setFee] = useState<FeeDetails | undefined>();
const [error, setError] = useState<string>();
const [bondedData, setBondedData] = useState<TBondedMixnode | TBondedGateway | null>(null);
const [bondedMixnode, setBondedMixnode] = useState<TBondedMixnode | null>(null);
const [bondedGateway, setBondedGateway] = useState<TBondedGateway | null>(null);
const [trigger, setTrigger] = useState<Date>(new Date());
const triggerStateUpdate = () => setTrigger(new Date());
const resetState = () => {
setIsLoading(true);
setError(undefined);
setBondedGateway(null);
setBondedMixnode(null);
};
// fake tauri request
const fetchBondingData: () => Promise<TBondedMixnode | TBondedGateway | null> = async () => {
await mockSleep(SLEEP_MS);
return bondedData;
};
const checkOwnership = async () => {};
const refresh = useCallback(async () => {
const bounded = await fetchBondingData();
if (bounded && 'stake' in bounded) {
setBondedMixnode(bounded);
}
if (bounded && !('stake' in bounded)) {
setBondedGateway(bounded);
}
setIsLoading(false);
}, [network]);
useEffect(() => {
resetState();
refresh();
}, [network, bondedData]);
const bondMixnode = async (): Promise<TransactionExecuteResult> => {
setIsLoading(true);
await mockSleep(SLEEP_MS);
setBondedData(bondedMixnodeMock);
setIsLoading(false);
return TxResultMock;
};
const bondGateway = async (): Promise<TransactionExecuteResult> => {
setIsLoading(true);
await mockSleep(SLEEP_MS);
setBondedData(bondedGatewayMock);
setIsLoading(false);
return TxResultMock;
};
const unbond = async (): Promise<TransactionExecuteResult> => {
setIsLoading(true);
await mockSleep(SLEEP_MS);
setBondedData(null);
setIsLoading(false);
return TxResultMock;
};
const redeemRewards = async (): Promise<TransactionExecuteResult | undefined> => {
setIsLoading(true);
await mockSleep(SLEEP_MS);
triggerStateUpdate();
setIsLoading(false);
return TxResultMock;
};
const compoundRewards = async (): Promise<TransactionExecuteResult | undefined> => {
setIsLoading(true);
await mockSleep(SLEEP_MS);
triggerStateUpdate();
setIsLoading(false);
return TxResultMock;
};
const updateMixnode = async (): Promise<TransactionExecuteResult> => {
setIsLoading(true);
await mockSleep(SLEEP_MS);
triggerStateUpdate();
setIsLoading(false);
return TxResultMock;
};
const bondMore = async (_signature: string, _additionalBond: DecCoin) => {
setIsLoading(true);
await mockSleep(SLEEP_MS);
triggerStateUpdate();
setIsLoading(false);
return TxResultMock;
};
const getFee = async (_feeOperation: any, _args: any) => {
setFeeLoading(true);
await mockSleep(SLEEP_MS);
setFeeLoading(false);
setFee(feeMock);
return feeMock;
};
const resetFeeState = () => {};
const memoizedValue = useMemo(
() => ({
isLoading,
error,
bondMixnode,
bondGateway,
unbond,
refresh,
redeemRewards,
compoundRewards,
fee,
feeLoading,
getFee,
resetFeeState,
updateMixnode,
bondMore,
checkOwnership,
}),
[isLoading, error, bondedMixnode, bondedGateway, trigger, fee],
);
return <BondingContext.Provider value={memoizedValue}>{children}</BondingContext.Provider>;
};
+3 -4
View File
@@ -26,21 +26,20 @@ export const ApplicationLayout: React.FC = ({ children }) => {
sx={{
background: (t) => t.palette.nym.nymWallet.nav.background,
overflow: 'auto',
py: 3,
px: 5,
py: 5,
}}
display="flex"
flexDirection="column"
justifyContent="space-between"
>
<Box>
<Box sx={{ mb: 4 }}>
<Box sx={{ ml: 5, mb: 3 }}>
<NymWordmark height={14} />
</Box>
<Nav />
</Box>
{appVersion && (
<Box color="#888" mt={8}>
<Box color="#888" ml={5} mt={8}>
Version {appVersion}
</Box>
)}
@@ -87,12 +87,12 @@ export const TransferModal = ({ onClose }: { onClose: () => void }) => {
) : (
<>
<ModalListItem
label="Unlocked transferrable tokens"
label="Unlocked transferrable tokens:"
value={`${userBalance.tokenAllocation?.spendable} ${clientDetails?.display_mix_denom.toUpperCase()}`}
divider
/>
<ModalListItem
label="Est. fee for this transaction"
label="Est. fee for this transaction:"
value={fee ? `${fee.amount?.amount} ${fee.amount?.denom}` : <CircularProgress size={15} />}
divider
/>
@@ -1,5 +1,6 @@
/* eslint-disable react/no-array-index-key */
import React, { useContext } from 'react';
import { useTheme } from '@mui/material/styles';
import { Box, Tooltip, Typography } from '@mui/material';
import { format } from 'date-fns';
import { AppContext } from '../../../context/main';
@@ -21,6 +22,9 @@ export const VestingTimeline: React.FC<{ percentageComplete: number }> = ({ perc
userBalance: { currentVestingPeriod, vestingAccountInfo },
} = useContext(AppContext);
const theme = useTheme();
const { mode } = theme.palette;
const nextPeriod =
typeof currentVestingPeriod === 'object' && !!vestingAccountInfo?.periods
? Number(vestingAccountInfo?.periods[currentVestingPeriod.In + 1]?.start_time)
@@ -29,19 +33,31 @@ export const VestingTimeline: React.FC<{ percentageComplete: number }> = ({ perc
return (
<Box display="flex" flexDirection="column" gap={1} position="relative" width="100%">
<svg width="100%" height="12">
<rect y="2" width="100%" height="6" rx="0" fill="#E6E6E6" />
<rect y="2" width={`${percentageComplete}%`} height="6" rx="0" fill="#121726" />
<rect y="2" width="100%" height="6" rx="0" fill="text.dark" />
<rect
y="2"
width={`${percentageComplete}%`}
height="6"
rx="0"
fill={mode === 'light' ? 'text.dark' : theme.palette.success.main}
/>
{vestingAccountInfo?.periods.map((period, i, arr) => (
<Marker
position={`${calculateMarkerPosition(arr.length, i)}%`}
color={+percentageComplete.toFixed(2) >= calculateMarkerPosition(arr.length, i) ? '#121726' : '#B9B9B9'}
color={
+percentageComplete.toFixed(2) >= calculateMarkerPosition(arr.length, i)
? mode === 'light'
? 'text.dark'
: theme.palette.success.main
: '#B9B9B9'
}
tooltipText={format(new Date(Number(period.start_time) * 1000), 'HH:mm do MMM yyyy')}
key={i}
/>
))}
<Marker
position="calc(100% - 4px)"
color={percentageComplete === 100 ? '#121726' : '#B9B9B9'}
color={percentageComplete === 100 ? (mode === 'light' ? 'text.dark' : theme.palette.success.main) : '#B9B9B9'}
tooltipText="End of vesting schedule"
/>
</svg>
+28 -4
View File
@@ -65,20 +65,44 @@ const VestingSchedule = () => {
))}
</TableRow>
<TableRow>
<TableCell sx={{ borderBottom: 'none', textTransform: 'uppercase' }}>
<TableCell
sx={{
color: (t) => (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'),
borderBottom: 'none',
textTransform: 'uppercase',
}}
>
{userBalance.tokenAllocation?.vesting || 'n/a'} / {userBalance.originalVesting?.amount.amount}{' '}
{clientDetails?.display_mix_denom.toUpperCase()}
</TableCell>
<TableCell align="left" sx={{ borderBottom: 'none' }}>
<TableCell
align="left"
sx={{
color: (t) => (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'),
borderBottom: 'none',
}}
>
{vestingPeriod(userBalance.currentVestingPeriod, userBalance.originalVesting?.number_of_periods)}
</TableCell>
<TableCell sx={{ borderBottom: 'none' }}>
<TableCell
sx={{
color: (t) => (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'),
borderBottom: 'none',
}}
>
<Box display="flex" alignItems="center" gap={1}>
<Typography variant="body2">{`${vestedPercentage}%`}</Typography>
<VestingTimeline percentageComplete={vestedPercentage} />
</Box>
</TableCell>
<TableCell sx={{ borderBottom: 'none', textTransform: 'uppercase' }} align="right">
<TableCell
sx={{
color: (t) => (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'),
borderBottom: 'none',
textTransform: 'uppercase',
}}
align="right"
>
{userBalance.tokenAllocation?.vested || 'n/a'} / {userBalance.originalVesting?.amount.amount}{' '}
{clientDetails?.display_mix_denom.toUpperCase()}
</TableCell>
@@ -20,8 +20,8 @@ export const ConfirmationModal = ({
}) => (
<SimpleModal header="Bond confirmation" open onOk={onConfirm} okLabel="Confirm" hideCloseIcon onBack={onPrev}>
<Box sx={{ mt: 3 }}>
<ModalListItem label="Mixnode identity" value={identity} />
<ModalListItem label="Amount" value={`${amount.amount} ${amount.denom}`} />
<ModalListItem label="Mixnode identity:" value={identity} />
<ModalListItem label="Amount:" value={`${amount.amount} ${amount.denom}`} />
<ModalFee fee={fee} isLoading={false} />
</Box>
</SimpleModal>
@@ -0,0 +1,13 @@
import * as React from 'react';
import { BondingPage } from './index';
import { MockBondingContextProvider } from '../../context/mocks/bonding';
export default {
title: 'Bonding/Flows/Mock',
};
export const Default = () => (
<MockBondingContextProvider>
<BondingPage />
</MockBondingContextProvider>
);
+252
View File
@@ -0,0 +1,252 @@
import React, { useContext, useState } from 'react';
import { FeeDetails } from '@nymproject/types';
import { TPoolOption } from 'src/components';
import { Bond } from 'src/components/Bonding/Bond';
import { BondedMixnode } from 'src/components/Bonding/BondedMixnode';
import { TBondedMixnodeActions } from 'src/components/Bonding/BondedMixnodeActions';
import { BondGatewayModal } from 'src/components/Bonding/modals/BondGatewayModal';
import { BondMixnodeModal } from 'src/components/Bonding/modals/BondMixnodeModal';
import { ConfirmationDetailProps, ConfirmationDetailsModal } from 'src/components/Bonding/modals/ConfirmationModal';
import { NodeSettings } from 'src/components/Bonding/modals/NodeSettingsModal';
import { UnbondModal } from 'src/components/Bonding/modals/UnbondModal';
import { ErrorModal } from 'src/components/Modals/ErrorModal';
import { LoadingModal } from 'src/components/Modals/LoadingModal';
import { AppContext, urls } from 'src/context/main';
import { isGateway, isMixnode, TBondGatewayArgs, TBondMixNodeArgs } from 'src/types';
import { BondedGateway } from 'src/components/Bonding/BondedGateway';
import { RedeemRewardsModal } from 'src/components/Bonding/modals/RedeemRewardsModal';
import { CompoundRewardsModal } from 'src/components/Bonding/modals/CompoundRewardsModal';
import { PageLayout } from '../../layouts';
import { BondingContextProvider, useBondingContext } from '../../context';
import { Box } from '@mui/material';
const Bonding = () => {
const [showModal, setShowModal] = useState<
'bond-mixnode' | 'bond-gateway' | 'bond-more' | 'unbond' | 'redeem' | 'compound' | 'node-settings'
>();
const [confirmationDetails, setConfirmationDetails] = useState<ConfirmationDetailProps>();
const {
network,
clientDetails,
userBalance: { originalVesting },
} = useContext(AppContext);
const {
bondedNode,
bondMixnode,
bondGateway,
unbond,
updateMixnode,
redeemRewards,
compoundRewards,
isLoading,
checkOwnership,
} = useBondingContext();
const handleCloseModal = async () => {
setShowModal(undefined);
await checkOwnership();
};
const handleError = (error: string) => {
setShowModal(undefined);
setConfirmationDetails({
status: 'error',
title: 'An error occurred',
subtitle: error,
});
};
const handleBondMixnode = async (data: TBondMixNodeArgs, tokenPool: TPoolOption) => {
setShowModal(undefined);
const tx = await bondMixnode(data, tokenPool);
setConfirmationDetails({
status: 'success',
title: 'Bond successful',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
return undefined;
};
const handleBondGateway = async (data: TBondGatewayArgs, tokenPool: TPoolOption) => {
setShowModal(undefined);
const tx = await bondGateway(data, tokenPool);
setConfirmationDetails({
status: 'success',
title: 'Bond successful',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
};
const handleUnbond = async (fee?: FeeDetails) => {
setShowModal(undefined);
const tx = await unbond(fee);
setConfirmationDetails({
status: 'success',
title: 'Unbond successful',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
};
const handleUpdateProfitMargin = async (profitMargin: number, fee?: FeeDetails) => {
setShowModal(undefined);
const tx = await updateMixnode(profitMargin, fee);
setConfirmationDetails({
status: 'success',
title: 'Profit margin update successful',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
};
const handleRedeemReward = async (fee?: FeeDetails) => {
setShowModal(undefined);
const tx = await redeemRewards(fee);
setConfirmationDetails({
status: 'success',
title: 'Rewards redeemed successfully',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
};
const handleCompoundReward = async (fee?: FeeDetails) => {
setShowModal(undefined);
const tx = await compoundRewards(fee);
setConfirmationDetails({
status: 'success',
title: 'Rewards compounded successfully',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
return undefined;
};
const handleBondedMixnodeAction = (action: TBondedMixnodeActions) => {
switch (action) {
case 'bondMore': {
setShowModal('bond-more');
break;
}
case 'unbond': {
setShowModal('unbond');
break;
}
case 'redeem': {
setShowModal('redeem');
break;
}
case 'compound': {
setShowModal('compound');
break;
}
case 'nodeSettings': {
setShowModal('node-settings');
break;
}
default: {
return undefined;
}
}
return undefined;
};
return (
<Box sx={{ mt: 4 }}>
{!bondedNode && <Bond disabled={isLoading} onBond={() => setShowModal('bond-mixnode')} />}
{bondedNode && isMixnode(bondedNode) && (
<BondedMixnode
mixnode={bondedNode}
network={network}
onActionSelect={(action) => handleBondedMixnodeAction(action)}
/>
)}
{bondedNode && isGateway(bondedNode) && (
<BondedGateway gateway={bondedNode} onActionSelect={handleBondedMixnodeAction} network={network} />
)}
{showModal === 'bond-mixnode' && (
<BondMixnodeModal
denom={clientDetails?.display_mix_denom || 'nym'}
hasVestingTokens={Boolean(originalVesting)}
onBondMixnode={handleBondMixnode}
onSelectNodeType={() => setShowModal('bond-gateway')}
onClose={() => setShowModal(undefined)}
onError={handleError}
/>
)}
{showModal === 'bond-gateway' && (
<BondGatewayModal
denom={clientDetails?.display_mix_denom || 'nym'}
hasVestingTokens={Boolean(originalVesting)}
onBondGateway={handleBondGateway}
onSelectNodeType={() => setShowModal('bond-mixnode')}
onClose={() => setShowModal(undefined)}
onError={handleError}
/>
)}
{showModal === 'unbond' && bondedNode && (
<UnbondModal
node={bondedNode}
onClose={() => setShowModal(undefined)}
onConfirm={handleUnbond}
onError={handleError}
/>
)}
{showModal === 'redeem' && bondedNode && isMixnode(bondedNode) && (
<RedeemRewardsModal
node={bondedNode}
onClose={() => setShowModal(undefined)}
onConfirm={handleRedeemReward}
onError={handleError}
/>
)}
{showModal === 'compound' && bondedNode && isMixnode(bondedNode) && (
<CompoundRewardsModal
node={bondedNode}
onClose={() => setShowModal(undefined)}
onConfirm={handleCompoundReward}
onError={handleError}
/>
)}
{showModal === 'node-settings' && bondedNode && isMixnode(bondedNode) && (
<NodeSettings
currentPm={bondedNode.profitMargin}
isVesting={Boolean(bondedNode.proxy)}
onConfirm={handleUpdateProfitMargin}
onClose={() => setShowModal(undefined)}
onError={handleError}
/>
)}
{confirmationDetails && confirmationDetails.status === 'success' && (
<ConfirmationDetailsModal
title={confirmationDetails.title}
subtitle={confirmationDetails.subtitle || 'This operation can take up to one hour to process'}
status={confirmationDetails.status}
txUrl={confirmationDetails.txUrl}
onClose={() => {
setConfirmationDetails(undefined);
handleCloseModal();
}}
/>
)}
{confirmationDetails && confirmationDetails.status === 'error' && (
<ErrorModal open message={confirmationDetails.subtitle} onClose={() => setConfirmationDetails(undefined)} />
)}
{isLoading && <LoadingModal />}
</Box>
);
};
export const BondingPage = () => (
<BondingContextProvider>
<Bonding />
</BondingContextProvider>
);
+70
View File
@@ -0,0 +1,70 @@
import { DecCoin, TNodeType, TransactionExecuteResult } from '@nymproject/types';
import { TPoolOption } from 'src/components';
export type FormStep = 1 | 2 | 3 | 4;
export type NodeType = TNodeType;
export type BondStatus = 'init' | 'success' | 'error' | 'loading';
export type ACTIONTYPE =
| { type: 'change_bond_type'; payload: NodeType }
| { type: 'set_node_data'; payload: NodeData }
| { type: 'set_amount_data'; payload: AmountData }
| { type: 'set_step'; payload: FormStep }
| { type: 'set_tx'; payload: TransactionExecuteResult | undefined }
| { type: 'set_error'; payload: string | null | undefined }
| { type: 'set_bond_status'; payload: BondStatus }
| { type: 'next_step' }
| { type: 'prev_step' }
| { type: 'show_modal' }
| { type: 'close_modal' }
| { type: 'reset' };
export type NodeIdentity = {
identityKey: string;
sphinxKey: string;
ownerSignature: string;
host: string;
version: string;
mixPort: number;
};
export type MixnodeData = NodeIdentity & {
verlocPort: number;
httpApiPort: number;
};
export type Amount = {
amount: DecCoin;
tokenPool: string;
};
export type GatewayAmount = Amount;
export type MixnodeAmount = Amount & {
profitMargin: number;
};
export type GatewayData = NodeIdentity & {
location: string;
clientsPort: number;
};
export type NodeData<N = MixnodeData | GatewayData> = {
nodeType: TNodeType;
} & N;
export interface AmountData {
amount: DecCoin;
tokenPool: TPoolOption;
profitMargin?: number;
}
export interface BondState {
showModal: boolean;
formStep: FormStep;
nodeData?: NodeData;
amountData?: MixnodeAmount | GatewayAmount;
tx?: TransactionExecuteResult;
bondStatus: BondStatus;
error?: string | null;
}
+3 -7
View File
@@ -138,7 +138,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
setConfirmationModalProps({
status: 'success',
action: 'delegate',
message: 'Delegations can take up to one hour to process',
message: 'This operation can take up to one hour to process',
...balances,
transactions: [
{ url: `${urls(network).blockExplorer}/transaction/${tx.transaction_hash}`, hash: tx.transaction_hash },
@@ -240,11 +240,9 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
try {
const txs = await claimRewards(identityKey, fee);
const bal = await userBalance();
setConfirmationModalProps({
status: 'success',
action: 'redeem',
balance: bal?.printable_balance || '-',
transactions: txs.map((tx) => ({
url: `${urls(network).blockExplorer}/transaction/${tx.transaction_hash}`,
hash: tx.transaction_hash,
@@ -270,11 +268,9 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
try {
const txs = await compoundRewards(identityKey, fee);
const bal = await userBalance();
setConfirmationModalProps({
status: 'success',
action: 'compound',
balance: bal?.printable_balance || '-',
transactions: txs.map((tx) => ({
url: `${urls(network).blockExplorer}/transaction/${tx.transaction_hash}`,
hash: tx.transaction_hash,
@@ -292,7 +288,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
return (
<>
<Paper elevation={0} sx={{ p: 4, mt: 4 }}>
<Paper elevation={0} sx={{ p: 3, mt: 4 }}>
<Stack spacing={5}>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">Delegations</Typography>
@@ -327,7 +323,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
</Paper>
{pendingDelegations && (
<Paper elevation={0} sx={{ p: 4, mt: 2 }}>
<Paper elevation={0} sx={{ p: 3, mt: 2 }}>
<Stack spacing={5}>
<Typography variant="h6">Pending Delegation Events</Typography>
<PendingEvents pendingEvents={pendingDelegations} explorerUrl={urls(network).networkExplorer} />
+4 -5
View File
@@ -1,8 +1,7 @@
export * from './auth';
export * from './Admin';
export * from './balance';
export * from './bond';
export * from './internal-docs';
export * from './auth';
export * from './settings';
export * from './unbond';
export * from './bonding';
export * from './delegation';
export * from './internal-docs';
export * from './unbond';
-67
View File
@@ -1,67 +0,0 @@
import React, { useContext, useEffect, useState } from 'react';
import { Alert, Box, Dialog } from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import { NymCard } from '../../components';
import { AppContext } from '../../context/main';
import { Tabs } from './tabs';
import { Profile } from './profile';
import { SystemVariables } from './system-variables';
import { NodeStats } from './node-stats';
import { useSettingsState } from './useSettingsState';
import { NodeStatus } from '../../components/NodeStatus';
import { Node as NodeIcon } from '../../svg-icons/node';
const tabs = ['Profile', 'System variables', 'Node stats'];
export const Settings = () => {
const [selectedTab, setSelectedTab] = useState(0);
const { mixnodeDetails, showSettings, getBondDetails, handleShowSettings } = useContext(AppContext);
const { status, saturation, rewardEstimation, inclusionProbability, updateAllMixnodeStats } = useSettingsState();
const handleTabChange = (_: React.SyntheticEvent, newTab: number) => setSelectedTab(newTab);
useEffect(() => {
getBondDetails();
if (mixnodeDetails) {
updateAllMixnodeStats(mixnodeDetails.mix_node.identity_key);
}
}, [showSettings, selectedTab]);
return showSettings ? (
<Dialog open onClose={handleShowSettings} maxWidth="md" fullWidth>
<NymCard
title={
<Box width="100%" display="flex" justifyContent="space-between">
<Box display="flex" alignItems="center">
<NodeIcon sx={{ mr: 1 }} />
Node Settings
</Box>
<CloseIcon onClick={handleShowSettings} cursor="pointer" />
</Box>
}
Action={<NodeStatus status={status} />}
dataTestid="node-settings"
noPadding
>
<>
<Tabs tabs={tabs} selectedTab={selectedTab} onChange={handleTabChange} disabled={!mixnodeDetails} />
{!mixnodeDetails && (
<Alert severity="info" sx={{ m: 4 }}>
You do not currently have a node running
</Alert>
)}
{selectedTab === 0 && <Profile />}
{selectedTab === 1 && (
<SystemVariables
saturation={saturation}
rewardEstimation={rewardEstimation}
inclusionProbability={inclusionProbability}
/>
)}
{selectedTab === 2 && mixnodeDetails && <NodeStats mixnodeId={mixnodeDetails.mix_node.identity_key} />}
</>
</NymCard>
</Dialog>
) : null;
};
+2 -2
View File
@@ -12,8 +12,8 @@ export const bondMixNode = async (args: TBondMixNodeArgs) =>
export const unbondMixNode = async (fee?: Fee) => invokeWrapper<TransactionExecuteResult>('unbond_mixnode', { fee });
export const updateMixnode = async (profitMarginPercent: number) =>
invokeWrapper<TransactionExecuteResult>('update_mixnode', { profitMarginPercent });
export const updateMixnode = async (profitMarginPercent: number, fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('update_mixnode', { profitMarginPercent, fee });
export const send = async (args: { amount: DecCoin; address: string; memo: string; fee?: Fee }) =>
invokeWrapper<SendTxResult>('send', args);
+9 -1
View File
@@ -6,8 +6,9 @@ import {
InclusionProbabilityResponse,
DecCoin,
MixNodeBond,
GatewayBond,
} from '@nymproject/types';
import { Epoch } from 'src/types';
import { Epoch, TNodeDescription } from 'src/types';
import { invokeWrapper } from './wrapper';
export const getReverseMixDelegations = async () =>
@@ -25,6 +26,7 @@ export const getAllPendingDelegations = async () =>
invokeWrapper<DelegationEvent[]>('get_all_pending_delegation_events');
export const getMixnodeBondDetails = async () => invokeWrapper<MixNodeBond | null>('mixnode_bond_details');
export const getGatewayBondDetails = async () => invokeWrapper<GatewayBond | null>('gateway_bond_details');
export const getOperatorRewards = async (address: string) =>
invokeWrapper<DecCoin>('get_operator_rewards', { address });
@@ -46,3 +48,9 @@ export const getInclusionProbability = async (identity: string) =>
invokeWrapper<InclusionProbabilityResponse>('mixnode_inclusion_probability', { identity });
export const getCurrentEpoch = async () => invokeWrapper<Epoch>('get_current_epoch');
export const getNumberOfMixnodeDelegators = async (identity: string) =>
invokeWrapper<number>('get_number_of_mixnode_delegators', { identity });
export const getNodeDescription = async (host: string, port: number) =>
invokeWrapper<TNodeDescription>('get_mix_node_description', { host, port });
+5 -4
View File
@@ -1,10 +1,11 @@
import { FeeDetails, TransactionExecuteResult } from '@nymproject/types';
import { Fee, FeeDetails, TransactionExecuteResult } from '@nymproject/types';
import { invokeWrapper } from './wrapper';
export const claimOperatorRewards = async () => invokeWrapper<TransactionExecuteResult[]>('claim_operator_reward');
export const claimOperatorReward = async (fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('claim_operator_reward', { fee });
export const compoundOperatorRewards = async () =>
invokeWrapper<TransactionExecuteResult[]>('compound_operator_reward');
export const compoundOperatorReward = async (fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('compound_operator_reward', { fee });
export const claimDelegatorRewards = async (mixIdentity: string, fee?: FeeDetails) =>
invokeWrapper<TransactionExecuteResult[]>('claim_locked_and_unlocked_delegator_reward', {
+14 -2
View File
@@ -12,7 +12,8 @@ export const simulateBondMixnode = async (args: TBondMixNodeArgs) =>
export const simulateUnbondMixnode = async (args: any) => invokeWrapper<FeeDetails>('simulate_unbond_mixnode', args);
export const simulateUpdateMixnode = async (args: any) => invokeWrapper<FeeDetails>('simulate_update_mixnode', args);
export const simulateUpdateMixnode = async (args: { profitMarginPercent: number }) =>
invokeWrapper<FeeDetails>('simulate_update_mixnode', args);
export const simulateDelegateToMixnode = async (args: { identity: string; amount: DecCoin }) =>
invokeWrapper<FeeDetails>('simulate_delegate_to_mixnode', args);
@@ -49,7 +50,7 @@ export const simulateVestingBondMixnode = async (args: { mixnode: MixNode; pledg
export const simulateVestingUnbondMixnode = async () => invokeWrapper<FeeDetails>('simulate_vesting_unbond_mixnode');
export const simulateVestingUpdateMixnode = async (args: any) =>
export const simulateVestingUpdateMixnode = async (args: { profitMarginPercent: number }) =>
invokeWrapper<FeeDetails>('simulate_vesting_update_mixnode', args);
export const simulateWithdrawVestedCoins = async (args: any) =>
@@ -57,3 +58,14 @@ export const simulateWithdrawVestedCoins = async (args: any) =>
export const simulateSend = async ({ address, amount }: { address: string; amount: DecCoin }) =>
invokeWrapper<FeeDetails>('simulate_send', { address, amount });
export const simulateClaimOperatorReward = async () => invokeWrapper<FeeDetails>('simulate_claim_operator_reward');
export const simulateVestingClaimOperatorReward = async () =>
invokeWrapper<FeeDetails>('simulate_vesting_claim_operator_reward');
export const simulateCompoundOperatorReward = async () =>
invokeWrapper<FeeDetails>('simulate_compound_operator_reward');
export const simulateVestingCompoundOperatorReward = async () =>
invokeWrapper<FeeDetails>('simulate_vesting_compound_operator_reward');
+6 -6
View File
@@ -60,8 +60,8 @@ export const vestingUnbondMixnode = async (fee?: Fee) =>
export const withdrawVestedCoins = async (amount: DecCoin, fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('withdraw_vested_coins', { amount, fee });
export const vestingUpdateMixnode = async (profitMarginPercent: number) =>
invokeWrapper<TransactionExecuteResult>('vesting_update_mixnode', { profitMarginPercent });
export const vestingUpdateMixnode = async (profitMarginPercent: number, fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('vesting_update_mixnode', { profitMarginPercent, fee });
export const vestingDelegateToMixnode = async ({
identity,
@@ -101,11 +101,11 @@ export const vestingUnbond = async (type: TNodeType) => {
return vestingUnbondGateway();
};
export const vestingClaimOperatorRewards = async () =>
invokeWrapper<TransactionExecuteResult>('vesting_claim_operator_reward');
export const vestingClaimOperatorReward = async (fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('vesting_claim_operator_reward', { fee });
export const vestingCompoundOperatorRewards = async () =>
invokeWrapper<TransactionExecuteResult>('vesting_compound_operator_reward');
export const vestingCompoundOperatorReward = async (fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('vesting_compound_operator_reward', { fee });
export const vestingClaimDelegatorRewards = async (mixIdentity: string) =>
invokeWrapper<TransactionExecuteResult>('vesting_claim_delegator_reward', { mixIdentity });
+2 -3
View File
@@ -4,17 +4,16 @@ import { ApplicationLayout } from 'src/layouts';
import { Terminal } from 'src/pages/terminal';
import { Send } from 'src/components/Send';
import { Receive } from '../components/Receive';
import { Bond, Balance, InternalDocs, Unbond, DelegationPage, Admin, Settings } from '../pages';
import { Balance, InternalDocs, Unbond, DelegationPage, Admin, BondingPage } from '../pages';
export const AppRoutes = () => (
<ApplicationLayout>
<Terminal />
<Settings />
<Send />
<Receive />
<Routes>
<Route path="/balance" element={<Balance />} />
<Route path="/bond" element={<Bond />} />
<Route path="/bonding" element={<BondingPage />} />
<Route path="/unbond" element={<Unbond />} />
<Route path="/delegation" element={<DelegationPage />} />
<Route path="/docs" element={<InternalDocs />} />
+22
View File
@@ -0,0 +1,22 @@
import React from 'react';
import { SvgIcon, SvgIconProps } from '@mui/material';
export const Bonding = (props: SvgIconProps) => (
<SvgIcon {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M16 17C14.8954 17 14 16.1046 14 15L14 9C14 7.89543 14.8954 7 16 7L22 7C23.1046 7 24 7.89543 24 9L24 15C24 16.1046 23.1046 17 22 17L16 17ZM16 9L16 15L22 15L22 9L16 9Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2 17C0.89543 17 -1.00647e-07 16.1046 -2.24801e-07 15L-8.99206e-07 9C-1.02336e-06 7.89543 0.895429 7 2 7L8 7C9.10457 7 10 7.89543 10 9L10 15C10 16.1046 9.10457 17 8 17L2 17ZM2 9L2 15L8 15L8 9L2 9Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6 12C6 11.4477 6.44772 11 7 11H17C17.5523 11 18 11.4477 18 12C18 12.5523 17.5523 13 17 13H7C6.44772 13 6 12.5523 6 12Z"
/>
</SvgIcon>
);
+1
View File
@@ -2,3 +2,4 @@ export * from './delegate';
export * from './undelegate';
export * from './bond';
export * from './unbond';
export * from './bonding';
+3
View File
@@ -64,6 +64,9 @@ declare module '@mui/material/styles' {
nav: {
background: string;
};
hover: {
background: string;
};
}
/**
+14 -8
View File
@@ -56,12 +56,15 @@ const darkMode: NymPaletteVariant = {
nav: {
background: '#292E34',
},
hover: {
background: '#36393E',
},
};
const lightMode: NymPaletteVariant = {
mode: 'light',
background: {
main: '#E5E5E5',
main: '#F4F6F8',
paper: '#FFFFFF',
warn: '#FFE600',
grey: '#F5F5F5',
@@ -80,6 +83,9 @@ const lightMode: NymPaletteVariant = {
nav: {
background: '#FFFFFF',
},
hover: {
background: '#F9F9F9',
},
};
/**
@@ -223,13 +229,6 @@ export const getDesignTokens = (mode: PaletteMode): ThemeOptions => {
},
},
components: {
MuiTypography: {
styleOverrides: {
root: {
fontSize: 14,
},
},
},
MuiButton: {
styleOverrides: {
root: {
@@ -273,6 +272,13 @@ export const getDesignTokens = (mode: PaletteMode): ThemeOptions => {
underline: 'none',
},
},
MuiDialogTitle: {
styleOverrides: {
root: {
fontWeight: 600,
},
},
},
},
palette,
};
+13
View File
@@ -1,5 +1,6 @@
import { Gateway, DecCoin, MixNode, PledgeData } from '@nymproject/types';
import { Fee } from '@nymproject/types/dist/types/rust/Fee';
import { TBondedGateway, TBondedMixnode } from 'src/context';
export enum EnumNodeType {
mixnode = 'mixnode',
@@ -39,6 +40,13 @@ export type TBondMixNodeArgs = {
fee?: Fee;
};
export type TNodeDescription = {
name: string;
description: string;
link: string;
location: string;
};
export type TDelegateArgs = {
identity: string;
amount: DecCoin;
@@ -51,3 +59,8 @@ export type TAccount = {
address: string;
mnemonic: string;
};
export const isMixnode = (node: TBondedMixnode | TBondedGateway): node is TBondedMixnode =>
(node as TBondedMixnode).profitMargin !== undefined;
export const isGateway = (node: TBondedMixnode | TBondedGateway): node is TBondedGateway => !isMixnode(node);
+2
View File
@@ -121,3 +121,5 @@ export const checkTokenBalance = async (tokenPool: TPoolOption, amount: string)
return hasEnoughFunds;
};
export const isDecimal = (value: number) => value - Math.floor(value) !== 0;
@@ -13,16 +13,21 @@ export const IdentityKeyFormField: React.FC<{
readOnly?: boolean;
initialValue?: string;
placeholder?: string;
label?: string;
helperText?: string;
onChanged?: (newValue: string) => void;
onValidate?: (isValid: boolean, error?: string) => void;
textFieldProps?: TextFieldProps;
errorText?: string;
sx?: SxProps;
}> = ({
required,
fullWidth,
placeholder,
label,
readOnly,
initialValue,
errorText,
sx,
onChanged,
onValidate,
@@ -55,7 +60,11 @@ export const IdentityKeyFormField: React.FC<{
if (initialValue) {
doValidation(initialValue);
}
}, [initialValue]);
if (errorText) {
setValidationError(errorText);
}
}, [initialValue, errorText]);
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const newValue = event.target.value;
@@ -82,6 +91,7 @@ export const IdentityKeyFormField: React.FC<{
),
}}
placeholder={placeholder}
label={label}
sx={sx}
{...textFieldProps}
aria-readonly={readOnly}
+1
View File
@@ -58,6 +58,7 @@ gateway-client = { path="../common/client-libs/gateway-client" }
mixnet-contract-common = { path= "../common/cosmwasm-smart-contracts/mixnet-contract" }
multisig-contract-common = { path = "../common/cosmwasm-smart-contracts/multisig-contract" }
nymsphinx = { path="../common/nymsphinx" }
task = { path = "../common/task" }
topology = { path="../common/topology" }
validator-api-requests = { path = "validator-api-requests" }
validator-client = { path="../common/client-libs/validator-client", features = ["nymd-client"] }
+17 -10
View File
@@ -14,6 +14,7 @@ use okapi::openapi3::OpenApi;
use rocket::Route;
use rocket_okapi::openapi_get_routes_spec;
use rocket_okapi::settings::OpenApiSettings;
use task::ShutdownListener;
use rocket::fairing::AdHoc;
use serde::Serialize;
@@ -252,20 +253,26 @@ impl<C> ValidatorCacheRefresher<C> {
Ok(())
}
pub(crate) async fn run(&self)
pub(crate) async fn run(&self, mut shutdown: ShutdownListener)
where
C: CosmWasmClient + Sync,
{
let mut interval = time::interval(self.caching_interval);
loop {
interval.tick().await;
if let Err(err) = self.refresh_cache().await {
error!("Failed to refresh validator cache - {}", err);
} else {
// relaxed memory ordering is fine here. worst case scenario network monitor
// will just have to wait for an additional backoff to see the change.
// And so this will not really incur any performance penalties by setting it every loop iteration
self.cache.initialised.store(true, Ordering::Relaxed)
while !shutdown.is_shutdown() {
tokio::select! {
_ = interval.tick() => {
if let Err(err) = self.refresh_cache().await {
error!("Failed to refresh validator cache - {}", err);
} else {
// relaxed memory ordering is fine here. worst case scenario network monitor
// will just have to wait for an additional backoff to see the change.
// And so this will not really incur any performance penalties by setting it every loop iteration
self.cache.initialised.store(true, Ordering::Relaxed)
}
}
_ = shutdown.recv() => {
trace!("UpdateHandler: Received shutdown");
}
}
}
}
+50 -12
View File
@@ -31,6 +31,7 @@ use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use std::{fs, process};
use task::ShutdownNotifier;
use tokio::sync::Notify;
// use validator_client::nymd::SigningNymdClient;
// use validator_client::ValidatorClientError;
@@ -226,14 +227,44 @@ fn parse_args<'a>() -> ArgMatches<'a> {
base_app.get_matches()
}
async fn wait_for_interrupt() {
if let Err(e) = tokio::signal::ctrl_c().await {
error!(
"There was an error while capturing SIGINT - {:?}. We will terminate regardless",
e
);
async fn wait_for_interrupt(mut shutdown: ShutdownNotifier) {
wait_for_signal().await;
log::info!("Sending shutdown");
shutdown.signal_shutdown().ok();
log::info!("Waiting for tasks to finish... (Press ctrl-c to force)");
shutdown.wait_for_shutdown().await;
log::info!("Stopping nym validator API");
}
#[cfg(unix)]
async fn wait_for_signal() {
use tokio::signal::unix::{signal, SignalKind};
let mut sigterm = signal(SignalKind::terminate()).expect("Failed to setup SIGTERM channel");
let mut sigquit = signal(SignalKind::quit()).expect("Failed to setup SIGQUIT channel");
tokio::select! {
_ = tokio::signal::ctrl_c() => {
log::info!("Received SIGINT");
},
_ = sigterm.recv() => {
log::info!("Received SIGTERM");
}
_ = sigquit.recv() => {
log::info!("Received SIGQUIT");
}
}
}
#[cfg(not(unix))]
async fn wait_for_signal() {
tokio::select! {
_ = tokio::signal::ctrl_c() => {
log::info!("Received SIGINT");
},
}
println!("Received SIGINT - the network monitor will terminate now");
}
fn setup_logging() {
@@ -547,6 +578,7 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> {
let signing_nymd_client = Client::new_signing(&config);
let liftoff_notify = Arc::new(Notify::new());
let shutdown = ShutdownNotifier::default();
// let's build our rocket!
let rocket = setup_rocket(
@@ -569,7 +601,8 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> {
// setup our daily uptime updater. Note that if network monitor is disabled, then we have
// no data for the updates and hence we don't need to start it up
let uptime_updater = HistoricalUptimeUpdater::new(storage.clone());
tokio::spawn(async move { uptime_updater.run().await });
let shutdown_listener = shutdown.subscribe();
tokio::spawn(async move { uptime_updater.run(shutdown_listener).await });
// spawn the cache refresher
let validator_cache_refresher = ValidatorCacheRefresher::new(
@@ -578,7 +611,8 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> {
validator_cache.clone(),
Some(storage.clone()),
);
tokio::spawn(async move { validator_cache_refresher.run().await });
let shutdown_listener = shutdown.subscribe();
tokio::spawn(async move { validator_cache_refresher.run(shutdown_listener).await });
// spawn rewarded set updater
let mut rewarded_set_updater =
@@ -593,11 +627,15 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> {
None,
);
let shutdown_listener = shutdown.subscribe();
// spawn our cacher
tokio::spawn(async move { validator_cache_refresher.run().await });
tokio::spawn(async move { validator_cache_refresher.run(shutdown_listener).await });
}
// launch the rocket!
// Rocket handles shutdown on it's own, but its shutdown handling should be incorporated
// with that of the rest of the tasks.
// Currently it's runtime is forcefully terminated once the validator-api exits.
let shutdown_handle = rocket.shutdown();
tokio::spawn(rocket.launch());
@@ -610,12 +648,12 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> {
// we're ready to go! spawn the network monitor!
let runnables = monitor_builder.build().await;
runnables.spawn_tasks();
runnables.spawn_tasks(&shutdown);
} else {
info!("Network monitoring is disabled.");
}
wait_for_interrupt().await;
wait_for_interrupt(shutdown).await;
shutdown_handle.notify();
Ok(())
+6 -3
View File
@@ -6,6 +6,7 @@ use crypto::asymmetric::{encryption, identity};
use futures::channel::mpsc;
use gateway_client::bandwidth::BandwidthController;
use std::sync::Arc;
use task::ShutdownNotifier;
use crate::config::Config;
use crate::contract_cache::ValidatorCache;
@@ -131,11 +132,13 @@ impl NetworkMonitorRunnables {
// TODO: note, that is not exactly doing what we want, because when
// `ReceivedProcessor` is constructed, it already spawns a future
// this needs to be refactored!
pub(crate) fn spawn_tasks(self) {
pub(crate) fn spawn_tasks(self, shutdown: &ShutdownNotifier) {
let mut packet_receiver = self.packet_receiver;
let mut monitor = self.monitor;
tokio::spawn(async move { packet_receiver.run().await });
tokio::spawn(async move { monitor.run().await });
let shutdown_listener = shutdown.subscribe();
tokio::spawn(async move { packet_receiver.run(shutdown_listener).await });
let shutdown_listener = shutdown.subscribe();
tokio::spawn(async move { monitor.run(shutdown_listener).await });
}
}
@@ -12,6 +12,7 @@ use crate::storage::ValidatorApiStorage;
use log::{debug, error, info};
use std::collections::{HashMap, HashSet};
use std::process;
use task::ShutdownListener;
use tokio::time::{sleep, Duration, Instant};
pub(crate) mod gateway_clients_cache;
@@ -296,7 +297,7 @@ impl Monitor {
self.test_nonce += 1;
}
pub(crate) async fn run(&mut self) {
pub(crate) async fn run(&mut self, mut shutdown: ShutdownListener) {
self.received_processor.start_receiving();
// wait for validator cache to be ready
@@ -308,9 +309,13 @@ impl Monitor {
.spawn_gateways_pinger(self.gateway_ping_interval);
let mut run_interval = tokio::time::interval(self.run_interval);
loop {
run_interval.tick().await;
self.test_run().await;
while !shutdown.is_shutdown() {
tokio::select! {
_ = run_interval.tick() => self.test_run().await,
_ = shutdown.recv() => {
trace!("UpdateHandler: Received shutdown");
}
}
}
}
}
@@ -7,6 +7,7 @@ use crypto::asymmetric::identity;
use futures::channel::mpsc;
use futures::StreamExt;
use gateway_client::{AcknowledgementReceiver, MixnetMessageReceiver};
use task::ShutdownListener;
pub(crate) type GatewayClientUpdateSender = mpsc::UnboundedSender<GatewayClientUpdate>;
pub(crate) type GatewayClientUpdateReceiver = mpsc::UnboundedReceiver<GatewayClientUpdate>;
@@ -55,8 +56,8 @@ impl PacketReceiver {
.expect("packet processor seems to have crashed!");
}
pub(crate) async fn run(&mut self) {
loop {
pub(crate) async fn run(&mut self, mut shutdown: ShutdownListener) {
while !shutdown.is_shutdown() {
tokio::select! {
// unwrap here is fine as it can only return a `None` if the PacketSender has died
// and if that was the case, then the entire monitor is already in an undefined state
@@ -67,6 +68,9 @@ impl PacketReceiver {
Some((_gateway_id, message)) = self.gateways_reader.stream_map().next() => {
self.process_gateway_messages(message)
}
_ = shutdown.recv() => {
trace!("UpdateHandler: Received shutdown");
}
}
}
}
@@ -7,6 +7,7 @@ use crate::node_status_api::models::{
use crate::node_status_api::ONE_DAY;
use crate::storage::ValidatorApiStorage;
use log::error;
use task::ShutdownListener;
use time::OffsetDateTime;
use tokio::time::sleep;
@@ -67,18 +68,23 @@ impl HistoricalUptimeUpdater {
Ok(())
}
pub(crate) async fn run(&self) {
loop {
// start any updates a day after starting the task so that we would have complete data
sleep(ONE_DAY).await;
if let Err(err) = self.update_uptimes().await {
// normally that would have been a warning rather than an error,
// however, in this case it implies some underlying issues with our database
// that might affect the entire program
error!(
"We failed to update daily uptimes of active nodes - {}",
err
)
pub(crate) async fn run(&self, mut shutdown: ShutdownListener) {
while !shutdown.is_shutdown() {
tokio::select! {
_ = sleep(ONE_DAY) => {
if let Err(err) = self.update_uptimes().await {
// normally that would have been a warning rather than an error,
// however, in this case it implies some underlying issues with our database
// that might affect the entire program
error!(
"We failed to update daily uptimes of active nodes - {}",
err
)
}
}
_ = shutdown.recv() => {
trace!("UpdateHandler: Received shutdown");
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More