diff --git a/package.json b/package.json index 8c8775444..c49cad9e4 100644 --- a/package.json +++ b/package.json @@ -34,9 +34,12 @@ "@types/iban": "^0.0.32", "@unstoppabledomains/resolution": "^5.0.1", "@vue/composition-api": "^0.4.0", + "chart.js": "^3.1.1", + "chartjs-adapter-luxon": "^1.0.0", "core-js": "^3.6.4", "iban": "^0.0.14", "idb-keyval": "^5.0.2", + "luxon": "^1.27.0", "pinia": "^0.0.5", "promise.allsettled": "^1.0.2", "register-service-worker": "^1.7.0", diff --git a/public/img/staking/arrow-right.svg b/public/img/staking/arrow-right.svg new file mode 100644 index 000000000..d259e8b8e --- /dev/null +++ b/public/img/staking/arrow-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/staking/background-collar.svg b/public/img/staking/background-collar.svg new file mode 100644 index 000000000..afe94207a --- /dev/null +++ b/public/img/staking/background-collar.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/staking/dot.svg b/public/img/staking/dot.svg new file mode 100644 index 000000000..f500223b5 --- /dev/null +++ b/public/img/staking/dot.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/staking/estimated-rewards-projection.svg b/public/img/staking/estimated-rewards-projection.svg new file mode 100644 index 000000000..0d9687f83 --- /dev/null +++ b/public/img/staking/estimated-rewards-projection.svg @@ -0,0 +1 @@ + diff --git a/public/img/staking/graph-preview-black.png b/public/img/staking/graph-preview-black.png new file mode 100644 index 000000000..125583447 Binary files /dev/null and b/public/img/staking/graph-preview-black.png differ diff --git a/public/img/staking/graph-preview-black.svg b/public/img/staking/graph-preview-black.svg new file mode 100644 index 000000000..48773b25e --- /dev/null +++ b/public/img/staking/graph-preview-black.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/staking/graph-preview-white.png b/public/img/staking/graph-preview-white.png new file mode 100644 index 000000000..66175d237 Binary files /dev/null and b/public/img/staking/graph-preview-white.png differ diff --git a/public/img/staking/graph-preview-white.svg b/public/img/staking/graph-preview-white.svg new file mode 100644 index 000000000..8d94f55fb --- /dev/null +++ b/public/img/staking/graph-preview-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/staking/providers/ace-staking.png b/public/img/staking/providers/ace-staking.png new file mode 100644 index 000000000..96fbf13fe Binary files /dev/null and b/public/img/staking/providers/ace-staking.png differ diff --git a/public/img/staking/providers/icestaking.png b/public/img/staking/providers/icestaking.png new file mode 100644 index 000000000..e6a67cf97 Binary files /dev/null and b/public/img/staking/providers/icestaking.png differ diff --git a/public/img/staking/providers/nimiq-watch.svg b/public/img/staking/providers/nimiq-watch.svg new file mode 100644 index 000000000..0e83dbeeb --- /dev/null +++ b/public/img/staking/providers/nimiq-watch.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/img/staking/providers/nimpool.svg b/public/img/staking/providers/nimpool.svg new file mode 100644 index 000000000..345c8b6cf --- /dev/null +++ b/public/img/staking/providers/nimpool.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/img/staking/providers/overstake.svg b/public/img/staking/providers/overstake.svg new file mode 100644 index 000000000..3fa9f4964 --- /dev/null +++ b/public/img/staking/providers/overstake.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/img/staking/staking.svg b/public/img/staking/staking.svg new file mode 100644 index 000000000..41238e969 --- /dev/null +++ b/public/img/staking/staking.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/img/staking/three-leaf-staking.svg b/public/img/staking/three-leaf-staking.svg new file mode 100644 index 000000000..0f7b19d1d --- /dev/null +++ b/public/img/staking/three-leaf-staking.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/mockups/Choose Validator.png b/public/mockups/Choose Validator.png new file mode 100644 index 000000000..684d975ec Binary files /dev/null and b/public/mockups/Choose Validator.png differ diff --git a/public/mockups/Overview.png b/public/mockups/Overview.png new file mode 100644 index 000000000..f43db321f Binary files /dev/null and b/public/mockups/Overview.png differ diff --git a/public/mockups/Set Amount.png b/public/mockups/Set Amount.png new file mode 100644 index 000000000..74a390113 Binary files /dev/null and b/public/mockups/Set Amount.png differ diff --git a/public/mockups/Staking Details.png b/public/mockups/Staking Details.png new file mode 100644 index 000000000..75d89bc2a Binary files /dev/null and b/public/mockups/Staking Details.png differ diff --git a/public/mockups/Welcome.png b/public/mockups/Welcome.png new file mode 100644 index 000000000..e79a487e6 Binary files /dev/null and b/public/mockups/Welcome.png differ diff --git a/public/mockups/[mobile]AccountOverview.png b/public/mockups/[mobile]AccountOverview.png new file mode 100644 index 000000000..5b5490f66 Binary files /dev/null and b/public/mockups/[mobile]AccountOverview.png differ diff --git a/public/mockups/[mobile]Graph.png b/public/mockups/[mobile]Graph.png new file mode 100644 index 000000000..ba63852f1 Binary files /dev/null and b/public/mockups/[mobile]Graph.png differ diff --git a/public/mockups/[mobile]GraphCenterTooltip.png b/public/mockups/[mobile]GraphCenterTooltip.png new file mode 100644 index 000000000..fa8f72e96 Binary files /dev/null and b/public/mockups/[mobile]GraphCenterTooltip.png differ diff --git a/public/mockups/[mobile]Intro.png b/public/mockups/[mobile]Intro.png new file mode 100644 index 000000000..b44a0294c Binary files /dev/null and b/public/mockups/[mobile]Intro.png differ diff --git a/public/mockups/[mobile]ValidatorConfirm.png b/public/mockups/[mobile]ValidatorConfirm.png new file mode 100644 index 000000000..5ff78ca1e Binary files /dev/null and b/public/mockups/[mobile]ValidatorConfirm.png differ diff --git a/public/mockups/[mobile]Validators.png b/public/mockups/[mobile]Validators.png new file mode 100644 index 000000000..82cf052c7 Binary files /dev/null and b/public/mockups/[mobile]Validators.png differ diff --git a/src/assets/staking/background-collar.svg b/src/assets/staking/background-collar.svg new file mode 100644 index 000000000..afe94207a --- /dev/null +++ b/src/assets/staking/background-collar.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/staking/providers/ace-staking.png b/src/assets/staking/providers/ace-staking.png new file mode 100644 index 000000000..96fbf13fe Binary files /dev/null and b/src/assets/staking/providers/ace-staking.png differ diff --git a/src/assets/staking/providers/icestaking.png b/src/assets/staking/providers/icestaking.png new file mode 100644 index 000000000..e6a67cf97 Binary files /dev/null and b/src/assets/staking/providers/icestaking.png differ diff --git a/src/assets/staking/providers/nimiq-watch.svg b/src/assets/staking/providers/nimiq-watch.svg new file mode 100644 index 000000000..0e83dbeeb --- /dev/null +++ b/src/assets/staking/providers/nimiq-watch.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/assets/staking/providers/nimpool.svg b/src/assets/staking/providers/nimpool.svg new file mode 100644 index 000000000..345c8b6cf --- /dev/null +++ b/src/assets/staking/providers/nimpool.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/staking/providers/overstake.svg b/src/assets/staking/providers/overstake.svg new file mode 100644 index 000000000..3fa9f4964 --- /dev/null +++ b/src/assets/staking/providers/overstake.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/assets/staking/staking.svg b/src/assets/staking/staking.svg new file mode 100644 index 000000000..41238e969 --- /dev/null +++ b/src/assets/staking/staking.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/staking/three-leaf-staking.svg b/src/assets/staking/three-leaf-staking.svg new file mode 100644 index 000000000..0f7b19d1d --- /dev/null +++ b/src/assets/staking/three-leaf-staking.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/staking/tooltip-graph.svg b/src/assets/staking/tooltip-graph.svg new file mode 100644 index 000000000..30b683251 --- /dev/null +++ b/src/assets/staking/tooltip-graph.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/vertical-line.svg b/src/assets/vertical-line.svg new file mode 100644 index 000000000..6db7a37e3 --- /dev/null +++ b/src/assets/vertical-line.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/icons/Staking/AceStakingIcon.vue b/src/components/icons/Staking/AceStakingIcon.vue new file mode 100644 index 000000000..5a6d97806 --- /dev/null +++ b/src/components/icons/Staking/AceStakingIcon.vue @@ -0,0 +1,5 @@ + diff --git a/src/components/icons/Staking/FatSearchIcon.vue b/src/components/icons/Staking/FatSearchIcon.vue new file mode 100644 index 000000000..d8f9d91d5 --- /dev/null +++ b/src/components/icons/Staking/FatSearchIcon.vue @@ -0,0 +1,6 @@ + diff --git a/src/components/icons/Staking/IceStakingIcon.vue b/src/components/icons/Staking/IceStakingIcon.vue new file mode 100644 index 000000000..2b152312a --- /dev/null +++ b/src/components/icons/Staking/IceStakingIcon.vue @@ -0,0 +1,5 @@ + diff --git a/src/components/icons/Staking/NimiqWatchIcon.vue b/src/components/icons/Staking/NimiqWatchIcon.vue new file mode 100644 index 000000000..98716435f --- /dev/null +++ b/src/components/icons/Staking/NimiqWatchIcon.vue @@ -0,0 +1,52 @@ + diff --git a/src/components/icons/Staking/NimpoolIcon.vue b/src/components/icons/Staking/NimpoolIcon.vue new file mode 100644 index 000000000..b0e091b6c --- /dev/null +++ b/src/components/icons/Staking/NimpoolIcon.vue @@ -0,0 +1,25 @@ + diff --git a/src/components/icons/Staking/OneLeafStakingIcon.vue b/src/components/icons/Staking/OneLeafStakingIcon.vue new file mode 100644 index 000000000..05dc6b5b8 --- /dev/null +++ b/src/components/icons/Staking/OneLeafStakingIcon.vue @@ -0,0 +1,6 @@ + diff --git a/src/components/icons/Staking/OverstakeIcon.vue b/src/components/icons/Staking/OverstakeIcon.vue new file mode 100644 index 000000000..16c59fdf0 --- /dev/null +++ b/src/components/icons/Staking/OverstakeIcon.vue @@ -0,0 +1,11 @@ + diff --git a/src/components/icons/Staking/SearchIcon.vue b/src/components/icons/Staking/SearchIcon.vue new file mode 100644 index 000000000..16ad10488 --- /dev/null +++ b/src/components/icons/Staking/SearchIcon.vue @@ -0,0 +1,6 @@ + diff --git a/src/components/icons/Staking/StakingHeroIcon.vue b/src/components/icons/Staking/StakingHeroIcon.vue new file mode 100644 index 000000000..a061d2e67 --- /dev/null +++ b/src/components/icons/Staking/StakingHeroIcon.vue @@ -0,0 +1,17 @@ + diff --git a/src/components/icons/Staking/StakingIcon.vue b/src/components/icons/Staking/StakingIcon.vue new file mode 100644 index 000000000..7c948f70d --- /dev/null +++ b/src/components/icons/Staking/StakingIcon.vue @@ -0,0 +1,5 @@ + diff --git a/src/components/icons/Staking/StarIcon.vue b/src/components/icons/Staking/StarIcon.vue new file mode 100644 index 000000000..257f32e58 --- /dev/null +++ b/src/components/icons/Staking/StarIcon.vue @@ -0,0 +1,5 @@ + diff --git a/src/components/icons/Staking/ThreeLeafStakingIcon.vue b/src/components/icons/Staking/ThreeLeafStakingIcon.vue new file mode 100644 index 000000000..c9ed1bb20 --- /dev/null +++ b/src/components/icons/Staking/ThreeLeafStakingIcon.vue @@ -0,0 +1,8 @@ + diff --git a/src/components/icons/Staking/VerticalLineIcon.vue b/src/components/icons/Staking/VerticalLineIcon.vue new file mode 100644 index 000000000..fe7e04d47 --- /dev/null +++ b/src/components/icons/Staking/VerticalLineIcon.vue @@ -0,0 +1,5 @@ + diff --git a/src/components/icons/Staking/XCloseIcon.vue b/src/components/icons/Staking/XCloseIcon.vue new file mode 100644 index 000000000..c5f0211f6 --- /dev/null +++ b/src/components/icons/Staking/XCloseIcon.vue @@ -0,0 +1,5 @@ + diff --git a/src/components/layouts/AccountOverview.vue b/src/components/layouts/AccountOverview.vue index 913468e22..5e4ca64ac 100644 --- a/src/components/layouts/AccountOverview.vue +++ b/src/components/layouts/AccountOverview.vue @@ -43,6 +43,8 @@ + + + + diff --git a/src/components/stake/StakeAmountSlider.vue b/src/components/stake/StakeAmountSlider.vue new file mode 100644 index 000000000..93019ee79 --- /dev/null +++ b/src/components/stake/StakeAmountSlider.vue @@ -0,0 +1,559 @@ + + + + + diff --git a/src/components/stake/StakeGraphPage.vue b/src/components/stake/StakeGraphPage.vue new file mode 100644 index 000000000..1ae4c2820 --- /dev/null +++ b/src/components/stake/StakeGraphPage.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/src/components/stake/StakeInfoPage.vue b/src/components/stake/StakeInfoPage.vue new file mode 100644 index 000000000..3cb3fbef8 --- /dev/null +++ b/src/components/stake/StakeInfoPage.vue @@ -0,0 +1,175 @@ + + + + + diff --git a/src/components/stake/StakeModal.vue b/src/components/stake/StakeModal.vue new file mode 100644 index 000000000..8e47f767e --- /dev/null +++ b/src/components/stake/StakeModal.vue @@ -0,0 +1,126 @@ + + + + + diff --git a/src/components/stake/StakeRewardsHistoryPage.vue b/src/components/stake/StakeRewardsHistoryPage.vue new file mode 100644 index 000000000..41683b640 --- /dev/null +++ b/src/components/stake/StakeRewardsHistoryPage.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/src/components/stake/StakeValidatorFilter.vue b/src/components/stake/StakeValidatorFilter.vue new file mode 100644 index 000000000..e1a5acd6e --- /dev/null +++ b/src/components/stake/StakeValidatorFilter.vue @@ -0,0 +1,210 @@ + + + + + diff --git a/src/components/stake/StakeValidatorListItem.vue b/src/components/stake/StakeValidatorListItem.vue new file mode 100644 index 000000000..34903e69b --- /dev/null +++ b/src/components/stake/StakeValidatorListItem.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/src/components/stake/StakeValidatorPage.vue b/src/components/stake/StakeValidatorPage.vue new file mode 100644 index 000000000..f4c7a2496 --- /dev/null +++ b/src/components/stake/StakeValidatorPage.vue @@ -0,0 +1,179 @@ + + + + + diff --git a/src/components/stake/StakedAlreadyPage.vue b/src/components/stake/StakedAlreadyPage.vue new file mode 100644 index 000000000..5a1a21148 --- /dev/null +++ b/src/components/stake/StakedAlreadyPage.vue @@ -0,0 +1,346 @@ + + + + + diff --git a/src/components/stake/assets/staking.json b/src/components/stake/assets/staking.json new file mode 100644 index 000000000..acc23e0d8 --- /dev/null +++ b/src/components/stake/assets/staking.json @@ -0,0 +1,24 @@ +{ + "validatorLabelDisclaimer": "The validator is solely responsible for information provided above.", + "uptimeRules": [ + ["80", "4.68", "The validator is extremely unreliable, which leads to reduced rewards."], + ["94", "4.68", "The validator is unreliable, which might lead to reduced rewards."], + ["96", "4.68", "The validator was occasionally offline, which might lead to reduced rewards."], + ["99", "4.68", "The validator is consistantly available and likely to receive the full reward potential."], + ["Infinity", "4.68", "The validator is reliably available and will receive the full reward potential."] + ], + "ageRules": [ + ["1", "4.99", "The validator just started and might not be very experienced."], + ["2", "4.99", "The validator is relatively young and might not be very experienced."], + ["3", "4.99", "The validator is sufficiently experienced"], + ["6", "4.99", "The validator is decently experienced."], + ["Infinity", "4.99", "The validator is very experienced."] + ], + "dominanceRules": [ + ["Infinity", "4.28", "Do not stake with this validator!\n\nThe validator controls a substantial share of the total stake. This makes it a potential threat to the network."], + ["22", "4.28", "Staking with this validator is not recommended!\n\nThe validator controls a substantial share of the total stake. This makes it a potential threat to the network."], + ["18", "4.28", "The validator controls a large share of the total stake.\n\nConsider choosing a smaller validator to improve the network’s security."], + ["12", "4.28", "The validator controls a sizeable share of the total stake. You can support the network by staking with a smaller validator."], + ["-1", "4.28", "The validator controls a healthy share of the total stake. Staking with this validator improves the network’s security."] + ] +} \ No newline at end of file diff --git a/src/components/stake/assets/validators.mock.json b/src/components/stake/assets/validators.mock.json new file mode 100644 index 000000000..9a8a7455b --- /dev/null +++ b/src/components/stake/assets/validators.mock.json @@ -0,0 +1,82 @@ +[ + { + "address": "overstake", + "icon": "overstake.svg", + "label": "Overstake", + "trust": 5.0, + "payout": 43200000, + "reward": 4.9, + "description": "We denounce with righteous indignation and dislike men who are so beguiled and. Demoralized by the charms of pleasure of the moment, so blinded by desire, that.", + "link": "https://www.nimiq.com", + "uptime": 99, + "monthsOld": 2, + "dominance": 12, + "stakedAmount": 0, + "unclaimedReward": 0, + "unstakePending": false + }, + { + "address": "nimiq-watch", + "icon": "nimiq-watch.svg", + "label": "Nimiq Watch", + "trust": 4.99, + "payout": 43200000, + "reward": 5.1, + "description": "We denounce with righteous indignation and dislike men who are so beguiled and. Demoralized by the charms of pleasure of the moment, so blinded by desire, that.", + "link": "https://www.nimiq.com", + "uptime": 99, + "monthsOld": 48, + "dominance": 10, + "stakedAmount": 0, + "unclaimedReward": 0, + "unstakePending": false + }, + { + "address": "nimpool", + "icon": "nimpool.svg", + "label": "NimPool", + "trust": 4.98, + "payout": 604800000, + "reward": 2.4, + "description": "We denounce with righteous indignation and dislike men who are so beguiled and. Demoralized by the charms of pleasure of the moment, so blinded by desire, that.", + "link": "https://www.nimiq.com", + "uptime": 99, + "monthsOld": 48, + "dominance": 25, + "stakedAmount": 0, + "unclaimedReward": 0, + "unstakePending": false + }, + { + "address": "icestaking", + "icon": "icestaking.png", + "label": "Icestaking", + "trust": 1.28, + "payout": 86400000, + "reward": 8.3, + "description": "We denounce with righteous indignation and dislike men who are so beguiled and. Demoralized by the charms of pleasure of the moment, so blinded by desire, that.", + "link": "https://www.nimiq.com", + "uptime": 84, + "monthsOld": 48, + "dominance": 10, + "stakedAmount": 0, + "unclaimedReward": 0, + "unstakePending": false + }, + { + "address": "acestaking", + "icon": "ace-staking.png", + "label": "Ace Staking", + "trust": 1.28, + "payout": 21600000, + "reward": 7.1, + "description": "We denounce with righteous indignation and dislike men who are so beguiled and. Demoralized by the charms of pleasure of the moment, so blinded by desire, that.", + "link": "https://www.nimiq.com", + "uptime": 80, + "monthsOld": 48, + "dominance": 10, + "stakedAmount": 0, + "unclaimedReward": 0, + "unstakePending": false + } +] diff --git a/src/components/stake/graph/StakingGraph.vue b/src/components/stake/graph/StakingGraph.vue new file mode 100644 index 000000000..845861fdc --- /dev/null +++ b/src/components/stake/graph/StakingGraph.vue @@ -0,0 +1,241 @@ + + + + + diff --git a/src/components/stake/graph/lib/LICENSE b/src/components/stake/graph/lib/LICENSE new file mode 100644 index 000000000..9e3e7d24c --- /dev/null +++ b/src/components/stake/graph/lib/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Tyson McCarney + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/components/stake/graph/lib/Vue3ChartJs.vue b/src/components/stake/graph/lib/Vue3ChartJs.vue new file mode 100644 index 000000000..e8c74ac39 --- /dev/null +++ b/src/components/stake/graph/lib/Vue3ChartJs.vue @@ -0,0 +1,173 @@ + + diff --git a/src/components/stake/graph/lib/includes.ts b/src/components/stake/graph/lib/includes.ts new file mode 100644 index 000000000..5e8ebd45f --- /dev/null +++ b/src/components/stake/graph/lib/includes.ts @@ -0,0 +1,56 @@ +const chartJsEventNames = [ + 'install', + 'start', + 'stop', + 'beforeInit', + 'afterInit', + 'beforeUpdate', + 'afterUpdate', + 'beforeElementsUpdate', + 'reset', + 'beforeDatasetsUpdate', + 'afterDatasetsUpdate', + 'beforeDatasetUpdate', + 'afterDatasetUpdate', + 'beforeLayout', + 'afterLayout', + 'afterLayout', + 'beforeRender', + 'afterRender', + 'resize', + 'destroy', + 'uninstall', + 'afterTooltipDraw', + 'beforeTooltipDraw', +]; + +function generateEventObject(type: string, chartRef = null) { + // chart js allows some events to be cancelled if they return false + // this implements familiar logic to allow vue emitted chart events to be canceled + return { + type, + chartRef, + preventDefault() { + this._defaultPrevented = true; + }, + isDefaultPrevented() { + return !this._defaultPrevented; + }, + _defaultPrevented: false, + }; +} + +function generateChartJsEventListener(emit: any, event: Event) { + return { + [event.type]: () => { + emit(event.type, event); + return event.defaultPrevented; + }, + }; +} + +export { + chartJsEventNames, + generateEventObject, + generateChartJsEventListener, +}; diff --git a/src/components/stake/graph/plugins/StakingGraphPointsPlugin.ts b/src/components/stake/graph/plugins/StakingGraphPointsPlugin.ts new file mode 100644 index 000000000..51dda366e --- /dev/null +++ b/src/components/stake/graph/plugins/StakingGraphPointsPlugin.ts @@ -0,0 +1,62 @@ +const drawRoundedRectBox = (ctx: CanvasRenderingContext2D, text: string, x: number, y: number) => { + const width = text.length * 7; + const height = 10; + const r = 10; + const x0 = x - (width / 2.0) - r; + const y0 = y - (height / 2.0) - r; + ctx.fillStyle = 'rgba(0, 0, 0, 1.0)'; + + ctx.beginPath(); + ctx.moveTo(x0 + r, y0); + ctx.lineTo(x0 + r + width, y0); + ctx.quadraticCurveTo(x0 + (r * 2) + width, y0, x0 + (2 * r) + width, y0 + r); + ctx.lineTo(x0 + (2 * r) + width, y0 + height); + ctx.quadraticCurveTo(x0 + (r * 2) + width, y0 + r + height, x0 + width + r, y0 + r + height); + ctx.lineTo(x0 + r, y0 + r + height); + ctx.quadraticCurveTo(x0, y0 + r + height, x0, y0 + height); + ctx.lineTo(x0, y0 + r); + ctx.quadraticCurveTo(x0, y0, x0 + r, y0); + ctx.stroke(); + ctx.fillStyle = 'rgba(255, 255, 255, 1.0)'; + ctx.fill(); + ctx.closePath(); + ctx.fillStyle = 'rgba(33, 188, 165, 1.0)'; + ctx.fillText(text, x0 + r, y0 + (1.5 * r)); +}; + +const points = [ + { x: 195, y: 110 }, + { x: 370, y: 81 }, + { x: 545, y: 52 }, +]; + +const plugin = { + id: 'staking-points', + afterDatasetDraw: (chart: any, args: any) => { + if (args.index !== 0) return; + const ctx = chart.canvas.getContext('2d'); + ctx.save(); + ctx.font = 'bold 14px Muli'; + ctx.lineWidth = 3; + + ctx.strokeStyle = 'rgba(33, 188, 165, 1.0)'; + + const yRange = args.meta._scaleRanges.ymax - args.meta._scaleRanges.ymin; + + for (let i = 0; i < args.meta._dataset.data.length; i++) { + const point = args.meta._dataset.data[i]; + if (point) { + const y = (152 - 16) - ((point.y - args.meta._scaleRanges.ymin) / yRange) * 152; + const x = (point.y === 0) ? 0 : points[i - 1].x; + + const label = args.meta.data[i].$context.element.options.pointStyle; + if (label.length > 0) { + drawRoundedRectBox(ctx, label, x, y); + } + } + } + ctx.restore(); + }, +}; + +export default plugin; diff --git a/src/components/stake/partials/MonthMiniSwitchPartial.vue b/src/components/stake/partials/MonthMiniSwitchPartial.vue new file mode 100644 index 000000000..47304af0f --- /dev/null +++ b/src/components/stake/partials/MonthMiniSwitchPartial.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/src/components/stake/partials/StakeButtonPartial.vue b/src/components/stake/partials/StakeButtonPartial.vue new file mode 100644 index 000000000..ae3dd6c46 --- /dev/null +++ b/src/components/stake/partials/StakeButtonPartial.vue @@ -0,0 +1,129 @@ + + + + + diff --git a/src/components/stake/partials/StakePreviewPartial.vue b/src/components/stake/partials/StakePreviewPartial.vue new file mode 100644 index 000000000..cb72994ea --- /dev/null +++ b/src/components/stake/partials/StakePreviewPartial.vue @@ -0,0 +1,148 @@ + + + + + diff --git a/src/components/stake/partials/StakingSummaryMobilePartial.vue b/src/components/stake/partials/StakingSummaryMobilePartial.vue new file mode 100644 index 000000000..0c83163cb --- /dev/null +++ b/src/components/stake/partials/StakingSummaryMobilePartial.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/src/components/stake/tooltips/LabelTooltip.vue b/src/components/stake/tooltips/LabelTooltip.vue new file mode 100644 index 000000000..d73d47c37 --- /dev/null +++ b/src/components/stake/tooltips/LabelTooltip.vue @@ -0,0 +1,105 @@ + + + + + diff --git a/src/components/stake/tooltips/ValidatorRewardBubble.vue b/src/components/stake/tooltips/ValidatorRewardBubble.vue new file mode 100644 index 000000000..5d66a0578 --- /dev/null +++ b/src/components/stake/tooltips/ValidatorRewardBubble.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/src/components/stake/tooltips/ValidatorTrustScore.vue b/src/components/stake/tooltips/ValidatorTrustScore.vue new file mode 100644 index 000000000..0df495988 --- /dev/null +++ b/src/components/stake/tooltips/ValidatorTrustScore.vue @@ -0,0 +1,64 @@ + + + + + diff --git a/src/lib/Constants.ts b/src/lib/Constants.ts index 439ec6fde..1cc98a60d 100644 --- a/src/lib/Constants.ts +++ b/src/lib/Constants.ts @@ -23,3 +23,6 @@ export const BTC_MAX_COPYABLE_ADDRESSES = 5; // TODO: Update to 10 when BTC_ADDR export const BTC_UNCOPYABLE_ADDRESS_GAP = 1; export const OASIS_EUR_DETECTION_DELAY = 5; // minutes + +export const NIM_DECIMALS = 5; +export const NIM_MAGNITUDE = 1e5; diff --git a/src/lib/NumberFormatting.ts b/src/lib/NumberFormatting.ts index 959431fa1..e20ccb445 100644 --- a/src/lib/NumberFormatting.ts +++ b/src/lib/NumberFormatting.ts @@ -1,5 +1,7 @@ +import { FormattableNumber } from '@nimiq/utils'; import { useSettingsStore } from '../stores/Settings'; import { CryptoCurrency } from './Constants'; +import { i18n } from '../i18n/i18n-setup'; export function twoDigit(value: number) { if (value < 10) return `0${value}`; @@ -45,3 +47,70 @@ export function calculateDisplayedDecimals(amount: number | null, currency: Cryp if (amount < 10 * 1e5) return Math.max(decimals.value, 1); return decimals.value; } + +export function numberToLiteral(n: number): string { + // https://stackoverflow.com/questions/5529934/javascript-numbers-to-words refactored + const ones = ['', i18n.t('one'), 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine']; + const tens = ['', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety']; + const teens = ['ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', + 'sixteen', 'seventeen', 'eighteen', 'nineteen']; + + const magnitudes = [ + ['million', 1e6, null], + ['thousand', 1e3, null], + ['hundred', 1e2, null], + ['', 2e1, [tens, ones]], + ['', 1e1, [teens], 10], + ['', 0, [ones], 0], + ]; + + if (n === 0) return i18n.t('zero').toString(); + return magnitudes.reduce((result: string, mag: Array) => { + if (n >= mag[1]) { + if (mag[2] === null) { + result += i18n.t('{number} {magnitude}', { + number: numberToLiteral(Math.floor(n / mag[1])), + magnitude: mag[0], + }); + n %= mag[1]; + } else if (mag[2].length === 2) { + const hi = Math.floor(n / mag[2][0].length) - 1; + const lo = n % mag[2][1].length; + result += i18n.t('{hi} {lo}', { hi: mag[2][0][hi], lo: mag[2][1][lo] }); + } else if (mag[2].length === 1) { + result += i18n.t(mag[2][0][n - mag[3]]); + } + } + return result; + }, '').trim(); +} + +export function numberToLiteralTimes(n: number): string { + const timesTable = [ + i18n.t('zero times'), i18n.t('once'), i18n.t('twice'), i18n.t('thrice'), i18n.t('four times'), + i18n.t('five times'), i18n.t('six times'), i18n.t('seven times'), + i18n.t('eight times'), i18n.t('nine times'), + ]; + + if (timesTable[n]) return timesTable[n].toString(); + return i18n.t('{number} times', { number: n }).toString(); +} + +export function formatNumber(number: number, fractionDigits = 0): string { + return number.toFixed(fractionDigits);// .replace(/\B(?=(\d{3})+(?!\d))/g, '\'').trim(); +} + +export function formatAsNim(nim: number, fractionDigits = 0): string { + return `${formatNumber(nim, fractionDigits)} NIM`; +} + +export function formatLunaAsNim(luna: number, fractionDigits = 0): string { + return formatAsNim(Math.round(luna / 100000), fractionDigits); +} + +export const formatAmount = (value = 0, magnitude = 1): string => ( + new FormattableNumber(Math.round(value / magnitude)).toString({ + maxDecimals: 0, + useGrouping: true, + }) +); diff --git a/src/lib/StakingUtils.ts b/src/lib/StakingUtils.ts new file mode 100644 index 000000000..788f4c2a3 --- /dev/null +++ b/src/lib/StakingUtils.ts @@ -0,0 +1,32 @@ +import { i18n } from '../i18n/i18n-setup'; +import { numberToLiteralTimes } from './NumberFormatting'; + +export function getPayoutText(payout: number) { + const periods = { + year: (((3600 * 1000) * 24) * 30) * 12, + month: ((3600 * 1000) * 24) * 30, + week: ((3600 * 1000) * 24) * 7, + day: (3600 * 1000) * 24, + h: 3600 * 1000, + }; + let index = 0; + let value = 0; + const periodNames = Object.keys(periods); + + for (const [, period] of Object.entries(periods)) { + value = payout / period; + if (value >= 1) { + break; + } + index += 1; + } + + if (index === periodNames.length - 1) { + return i18n.t('pays out every {hourCount}', { hourCount: `${value}${periodNames[index]}` }) as string; + } + + return i18n.t('pays out {numberOfTimes} a {period}', { + numberOfTimes: numberToLiteralTimes(Math.floor(value)), + period: periodNames[index], + }) as string; +} diff --git a/src/router.ts b/src/router.ts index 26cddd01f..325b546d9 100644 --- a/src/router.ts +++ b/src/router.ts @@ -30,6 +30,8 @@ const DisclaimerModal = () => import(/* webpackChunkName: "disclaimer-modal" */ './components/modals/DisclaimerModal.vue'); const ReleaseNotesModal = () => import(/* webpackChunkName: "release-notes-modal" */ './components/modals/ReleaseNotesModal.vue'); +const StakeModal = () => + import(/* webpackChunkName: "stake-modal" */ './components/stake/StakeModal.vue'); // Bitcoin Modals const BtcActivationModal = () => @@ -224,6 +226,14 @@ const routes: RouteConfig[] = [{ name: 'simplex', // props: { modal: true }, meta: { column: Columns.DYNAMIC }, + }, { + path: '/stake', + components: { + modal: StakeModal, + }, + name: 'stake', + props: { modal: true }, + meta: { column: Columns.ACCOUNT }, // TODO: investigate usage }], }, { path: '/settings', diff --git a/src/storage.ts b/src/storage.ts index 0f3b30041..81abc64d6 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -16,6 +16,7 @@ import { useBtcTransactionsStore, Transaction as BtcTransaction } from './stores import { useBtcLabelsStore, BtcLabelsState } from './stores/BtcLabels'; import { useSwapsStore, SwapsState } from './stores/Swaps'; import { useBankStore, BankState } from './stores/Bank'; +import { StakingState, useStakingStore } from './stores/Staking'; const StorageKeys = { TRANSACTIONS: 'wallet_transactions_v01', @@ -29,6 +30,7 @@ const StorageKeys = { BTCADDRESSINFOS: 'wallet_btcaddresses_v01', SWAPS: 'wallet_swaps_v01', BANK: 'wallet_bank_v01', + STAKING: 'wallet_staking_dev_v01', }; const PersistentStorageKeys = { @@ -303,6 +305,21 @@ export async function initStorage() { unsubscriptions.push( bankStore.subscribe(() => Storage.set(StorageKeys.BANK, bankStore.state)), ); + + /** + * Staking + */ + const stakingStore = useStakingStore(); + const storedStakingState = await Storage.get(StorageKeys.STAKING); + if (storedStakingState) { + const partialState: Partial = storedStakingState; + delete partialState.validators; + stakingStore.patch(partialState); + } + + unsubscriptions.push( + stakingStore.subscribe(() => Storage.set(StorageKeys.STAKING, stakingStore.state)), + ); } export async function clearStorage() { diff --git a/src/stores/Staking.ts b/src/stores/Staking.ts new file mode 100644 index 000000000..b00fab988 --- /dev/null +++ b/src/stores/Staking.ts @@ -0,0 +1,118 @@ +import { createStore } from 'pinia'; +import { useAddressStore } from './Address'; + +import mockValidators from '../components/stake/assets/validators.mock.json'; + +export type StakingState = { + validators: {[id: string]: ValidatorData}, + addressStakes: {[address: string]: AddressStake}, +} + +export type AddressStake = { + address: string, + activeStake: number, + inactiveStake: number, + validator: string, +} + +export type RawValidator = { + address: string, + dominance: number, // Percentage +} + +export type RegisteredValidator = { + address: string, + label: string, + icon: string, + trust: number, + payout: number, + reward: number, + description: string, + link: string | null, + uptime: number, // Percentage + monthsOld: number, + dominance: number, // Percentage +} + +export type ValidatorData = RawValidator | RegisteredValidator; + +export type StakingScoringRules = any + +export type StakingData = { + validatorLabelDisclaimer: string, + uptimeRules: StakingScoringRules, + ageRules: StakingScoringRules, + dominanceRules: StakingScoringRules, +} + +export const useStakingStore = createStore({ + id: 'staking', + state: () => ({ + // TODO: Remove mock data, replace with loader on network init + validators: Object.fromEntries(mockValidators + .map((validator) => [validator.address, validator])), + addressStakes: {}, + } as StakingState), + getters: { + validatorsList: (state) => Object.values(state.validators), + activeStake: (state) => { + const { activeAddress } = useAddressStore(); + if (!activeAddress.value) return null; + return state.addressStakes[activeAddress.value] || null; + }, + activeValidator: (state, { activeStake }): ValidatorData | null => { + const stake = activeStake.value as AddressStake | null; + if (!stake) return null; + return state.validators[stake.validator] || null; + }, + }, + actions: { + setStake(stake: AddressStake) { + // Need to assign whole object for change detection of new addresses. + // TODO: Simply set new stake in Vue 3. + this.state.addressStakes = { + ...this.state.addressStakes, + [stake.address]: stake, + }; + }, + setStakes(stakes: AddressStake[]) { + const newStakes: {[address: string]: AddressStake} = {}; + + for (const stake of stakes) { + newStakes[stake.address] = stake; + } + + this.state.addressStakes = newStakes; + }, + patchStake(address: string, patch: Partial>) { + if (!this.state.addressStakes[address]) return; + + this.state.addressStakes[address] = { + ...this.state.addressStakes[address], + ...patch, + }; + }, + removeStake(address: string) { + const stakes = { ...this.state.addressStakes }; + delete stakes[address]; + this.state.addressStakes = stakes; + }, + setValidator(validator: ValidatorData) { + // Need to assign whole object for change detection of new addresses. + // TODO: Simply set new validator in Vue 3. + this.state.validators = { + ...this.state.validators, + [validator.address]: validator, + }; + }, + setValidators(validators: ValidatorData[]) { + const newValidators: {[address: string]: ValidatorData} = {}; + + for (const validator of validators) { + newValidators[validator.address] = validator; + } + + this.state.validators = newValidators; + }, + }, +}); diff --git a/yarn.lock b/yarn.lock index d039bf1d4..e9284f38e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3224,6 +3224,16 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== +chart.js@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-3.1.1.tgz#2cdbda7fccea532313332fe822f0cae268f24cf3" + integrity sha512-ghNJersc9VD9MECwa5bL8gqvCkndW6RSCicdEHL9lIriNtXwKawlSmwo+u6KNXLYT2+f24GdFPBoynKW3ke4MQ== + +chartjs-adapter-luxon@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/chartjs-adapter-luxon/-/chartjs-adapter-luxon-1.0.0.tgz#4f37164295859a468b64cc24ec2718356aa63d94" + integrity sha512-oy4iH4o6+8FqDoHYsEmUAB8+q8QO+vERDDHkmODm+MwWW1cet02wHN2KYvidwgBLYUX0laNWaWGN7l8Thac2Zg== + check-types@^8.0.3: version "8.0.3" resolved "https://registry.yarnpkg.com/check-types/-/check-types-8.0.3.tgz#3356cca19c889544f2d7a95ed49ce508a0ecf552" @@ -7112,6 +7122,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +luxon@^1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.27.0.tgz#ae10c69113d85dab8f15f5e8390d0cbeddf4f00f" + integrity sha512-VKsFsPggTA0DvnxtJdiExAucKdAnwbCCNlMM5ENvHlxubqWd0xhZcdb4XgZ7QFNhaRhilXCFxHuoObP5BNA4PA== + magic-string@^0.23.2: version "0.23.2" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.23.2.tgz#204d7c3ea36c7d940209fcc54c39b9f243f13369"