Compare commits

...

70 Commits

Author SHA1 Message Date
Jędrzej Stuczyński 24fe43b6d5 created basic scaffolding for any future TUI apps alongside a dummy example 2024-11-24 17:18:33 +00:00
Jon Häggblad 5cda49f996 Add derive_extended_private_key to DirectSecp256k1HdWallet (#5167) 2024-11-23 11:19:36 +01:00
Jon Häggblad 36657bcd97 Add support for DELETE to nym-http-api-client (#5166)
* Add delete support to http-api-client

* Add to trait
2024-11-23 10:42:49 +01:00
Jon Häggblad 6167243a10 Add export_to_env (#5162) 2024-11-22 17:50:43 +01:00
Jon Häggblad 920276f2ac Move two minor jobs to free tier github hosted runners (#5169) 2024-11-22 16:56:24 +01:00
Dinko Zdravac 1f53da7456 Fix env var name (#5165) 2024-11-22 15:41:16 +00:00
Jon Häggblad 2fdc0dc47b Add strum::EnumIter for TicketType (#5164) 2024-11-22 16:33:23 +01:00
Fran Arbanas a720c95dd7 feat: add env vars to NNM (#5163)
* feat: add env vars to NNM

* update version to testfran

* add ip and port for locust connection as variables

* bump version

* bump version, fix env variable names

* bump version to correct one
2024-11-22 14:32:58 +00:00
mx 013941dbaf patch curl command for network_tunnel_man.sh (#5160) 2024-11-21 20:28:22 +00:00
benedetta davico 1af6e1ecdd Create network_tunnel_manager.sh (#5156)
* Create network_tunnel_manager.sh

* update link for network tunnel manager script

* updating 2 more links
2024-11-21 20:11:28 +00:00
Mark Sinclair 379c1eb0d0 Update push-network-monitor.yaml 2024-11-21 16:11:59 +00:00
Mark Sinclair d1e91946e6 Update push-network-monitor.yaml 2024-11-21 16:07:12 +00:00
Mark Sinclair 402c79f2f5 Update push-network-monitor.yaml 2024-11-21 15:59:41 +00:00
Mark Sinclair 149b2f4e32 Update Cargo.toml for nym-network-monitor 2024-11-21 15:52:56 +00:00
Mark Sinclair 221e809da3 Update push-network-monitor.yaml 2024-11-21 15:52:11 +00:00
Mark Sinclair 760ee453ea Update nym-network-monitor.dockerfile 2024-11-21 15:25:38 +00:00
Dinko Zdravac 3f072e4e9d NS Agent auth with NS API (#5127)
* Agents authenticate with NSAPI

* /submit with better auth
- also adjust agent run script to authenticate, even in parallel

* /request better authentication
- moved agent API calls to Client struct

* Replay protection

* Fix testrun cleanup bug
- introduce a new column last_assigned which is different than
  created_at so that stale testruns get cleaned up based on
  last_assigned
- created_at is still useful for determining the "oldest" testrun
  to be picked up

* Uniform request authentication

* Suppress ts-rs serde warnings

* Update cargo version

* All agents use the same key
- remove assigned_agent column
- remove logic which would stop agents with
  the same key to connect
- as a safety measure, add cap to total no. of agents
2024-11-21 14:32:15 +01:00
mx aa460076f4 Max/surb doc update lifetime (#5154)
* updated surbs page with lifetimes info + attacks + diagram

* component build update

* update rewrites
2024-11-21 10:37:31 +00:00
benedetta davico b9500aacf3 Merge pull request #5153 from nymtech/merge/release/2024.13-magura
Merge/release/2024.13 magura
2024-11-20 11:38:49 +01:00
dependabot[bot] 3651663d1c build(deps): bump mikefarah/yq from 4.44.3 to 4.44.5 (#5149) 2024-11-20 11:01:59 +01:00
fmtabbara 4d43728059 fix linting 2024-11-20 09:44:59 +00:00
Jędrzej Stuczyński 7b1fbab9af fix: tracing logger to output to stderr 2024-11-19 17:47:34 +00:00
import this f3f8dd35ef [DOCS/operators]: Release changes v2024.13-magura & Tokenomics pages v1.0 (#5128)
* add directory changes to changelog

* exit gateway page update

* finish general tokenomics page

* add new operator toolings

* progressing with release notes

* add smart contract migration and bonding steps

* add new bonding steps

* add tokenomics roadmap

* added new pages to introduction - ready for review

* ready for review

* edit callout custom

* rm CalloutCustom.tsx

* syntax fix

* temp build version fix by hardcode

* built after review changes

* remove redundant point

* final commit - let's merge
2024-11-19 13:44:09 +00:00
Jędrzej Stuczyński 7bafe6583a Merge branch 'release/2024.13-magura' into merge/release/2024.13-magura 2024-11-19 15:02:21 +02:00
Simon Wicky 855eecf800 start session collection for exit gateways (#5148) 2024-11-19 09:30:58 +01:00
Tommy Verrall b49ef643df Update publish-nym-binaries.yml 2024-11-18 17:56:57 +01:00
Tommy Verrall 62e0771236 Update publish-nym-contracts.yml 2024-11-18 16:24:52 +01:00
Simon Wicky 05b55a1577 add version to clientStatsReport (#5147) 2024-11-18 16:02:48 +01:00
benedettadavico b5f1d674fe update wallet versions and changelog 2024-11-18 14:07:01 +01:00
benedettadavico 086b4f6f54 update changelog 2024-11-18 13:01:27 +01:00
Bogdan-Ștefan Neacşu 5ad11f2048 Limit race probability (#5145)
* Limit race probability

* Actually assign value
2024-11-18 13:33:19 +02:00
Simon Wicky 99e4ff9132 update serde_json_path due to compilation issue (#5144) 2024-11-18 11:45:45 +01:00
Jędrzej Stuczyński 6dc9b79ace bugifx: assign 'node_id' when converting from 'GatewayDetails' to 'TestNode' (#5143) 2024-11-18 10:28:46 +00:00
Jędrzej Stuczyński 35343b5220 bugfix: make sure to assign correct node_id and identity during 'gateway_details' table migration (#5142) 2024-11-18 10:00:40 +00:00
Bogdan-Ștefan Neacşu e44a36e5b5 Respond to auth messages with same version (#5140)
* Introduce traits for response

* Ugly responde with same protocol version

* Don't pull sdk crate unnecessarily
2024-11-18 11:21:07 +02:00
Mark Sinclair db20c2e2fa node-status-agent: cherry-pick GH Actions pipeline and dockerfile from 9c680fd 2024-11-15 17:55:07 +00:00
Mark Sinclair 94f247563b nym-node-status-agent bump version 2024-11-15 17:45:26 +00:00
Jędrzej Stuczyński 827a13523c bugfix: additional checks inside credential proxy (#5072)
* fix expiration date bound checks

* return inner error when querying for shares
2024-11-15 14:52:28 +00:00
Jędrzej Stuczyński 69718db6d2 chore: remove standalone legacy mixnode/gateway binaries (#5135)
* remove standalone gateway overhead

* remove standalone mixnode overhead

* additional cleanup: removed unused dependencies et al.

* removed calls to 'log::'
2024-11-15 12:37:35 +00:00
Simon Wicky 475a01c089 prepare vpn client country reporting (#5134) 2024-11-15 13:32:14 +01:00
Fran Arbanas 01e6a77cf1 feat: add whitelist overrides to rewarder validator (#5138)
* feat: add whitelist overrides to rewarder validator

* cleanup by using account id instead of string
2024-11-15 13:18:40 +01:00
Jędrzej Stuczyński a348ff43b0 feature: rewarding for ticketbook issuance (#5112)
* fixed pagination for querying for validators

* wip: decoupling block signing from ticketbook issuance

* added ecash contract query for latest deposit

* parking the branch: wrappers for merkle tree for issued ticketbooks

* make nym-api store merkle trees of issued ticketbooks

* nym-api route for returning all deposits alongside merkle root

* return index alongside deposit id

* persisting merkle index alongside issued ticketbook details

* wip

* responses for issued deposit challenges

* nym-api cleanup

* verification of issued partial ticketbooks

* cleanup of rewarder code

* make the rest of codebase compile

* updated config file

* improved logging

* fixed division by zero if there were no ticketbooks issued in a day

* using correct budget when rewarding operators

* fixed routes for issued data

* fixed ecash test fixture

* fixed incorrect deserialisation of expiration_date param

* additional bugfixes for ticketbook issuance

* more fixes and updated tests

* fixed formatting after rebasing

* updated schema

* fixed edge case unit test
2024-11-14 16:55:02 +00:00
Jędrzej Stuczyński 6809f7302e Pain/polyfill deprecated endpoints (#5131)
* polyfilled contract cache endpoints

* polyfilled legacy described endpoints
2024-11-14 15:32:20 +00:00
Simon Wicky bea4eb5cb0 [Product data] Data consumption with ecash ticket (#5120)
* add ticket report

* fix wasm client
2024-11-14 15:38:44 +01:00
Jędrzej Stuczyński fa45b5e564 removed ci-nym-api-tests.yml which was running outdated (and broken) tests (#5133) 2024-11-14 14:21:42 +00:00
Jon Häggblad 1a64442d9c Fix json syntax in CI file 2024-11-14 15:04:21 +01:00
Jędrzej Stuczyński 46d8206713 feature: config score (#5117)
* added config-score related parameters to the mixnet contract

* weaved in described_cache into NodeStatusCacheRefresher

* adding config score annotation

* using new updated performance for updating rewarded set

* using new values for rewarding

* clippy

* updated contract schema

* wallet fixes

* fixed wasm build
2024-11-14 13:57:41 +00:00
Simon Wicky b8c1014fea [Product Data] Config deserialization bug fix (#5126)
* fix no address deserialization bug

* bug fix in stats_id generation

* better stats id generation

* andrew's nitpicking
2024-11-14 13:25:14 +00:00
Jon Häggblad 43e4224f53 Merge pull request #5132 from nymtech/jon/ci-reduce-jobs
CI: reduce jobs running on cluster
2024-11-14 13:22:04 +01:00
Jon Häggblad d2817d6782 Remove commented out code 2024-11-14 13:19:31 +01:00
Jon Häggblad e97c94ef9e Disable pull_request trigger for ci-contracts-upload-binaries 2024-11-14 13:17:00 +01:00
Jon Häggblad aa919a5351 Disable pull_request trigger for ci-build-upload-binaries 2024-11-14 13:16:39 +01:00
Jon Häggblad 7617675dcc Disable beta toolchain CI builds for contracts 2024-11-14 13:16:12 +01:00
Jon Häggblad 276925814f Move ci-cargo-deny to free tier gh hosted runner 2024-11-14 13:15:50 +01:00
Andrej Mihajlov 46a33b5ef6 Add NYM_VPN_API to env files (#5099)
* Add missing NYM_VPN_API uri to environment files

* Add trailing slashes

---------

Co-authored-by: Jon Häggblad <jon.haggblad@gmail.com>
2024-11-14 13:05:03 +01:00
Jędrzej Stuczyński 532c25c4f5 change: dont allow mixnodes bonded with vested tokens into the rewarded set (#5129) 2024-11-14 08:48:06 +00:00
Jędrzej Stuczyński 8f9c26e7a6 bugfix: don't send empty BankMsg (#5121) 2024-11-13 16:10:50 +00:00
Fouad c0aadebf80 Migrate node when events pending (#5125)
* dont show node migration if there are vesting tokens

* catch and set errors
2024-11-13 12:53:57 +00:00
benedettadavico 5b216e8d40 update versions 2024-11-13 11:23:07 +01:00
Tommy Verrall 4fab7eac3f temporarily disable playground and test my node in the wallet
once we have time to fix these we will import these again
2024-11-13 10:56:19 +01:00
Jon Häggblad ac77712cc0 nym-credential-proxy-requests: reqwest use rustls-tls (#5116)
* nym-credential-proxy-requests: reqwest use rustls-tls

* nym-credential-proxy: reqwest default-features false
2024-11-11 17:38:21 +01:00
Jędrzej Stuczyński a400aa8928 bugfix: preserve as much as possible of the rewarded set during migration (#5103) 2024-11-08 09:33:30 +00:00
Jędrzej Stuczyński c001059af9 Feature/force refresh node (#5101)
* introduced nym-api endpoint for force refreshing described node data

* client code + updated return types

* nym-node to update self-described data cache on startup + change request type

* send request to all available nym-apis

* fixed 'is_stale' check
2024-11-06 09:17:44 +00:00
Jędrzej Stuczyński fd8dc63c88 fixed HistoricalUptimeUpdater (#5097) 2024-11-05 14:40:50 +00:00
Dinko Zdravac d03c5b3650 Graceful agent 1.1.5 (#5093)
* Bump NS agent to 0.1.5

* API improvements
- agent exits gracefully when no testrun available
- API doesn't log every error

* Bump NSAPI to 0.1.6
2024-11-05 15:36:16 +01:00
Bogdan-Ștefan Neacşu 69e97b3bbc Remove old use of 1GB constant (#5096)
* Remove old use of 1GB constant

* Fix clippy
2024-11-05 16:16:59 +02:00
Bogdan-Ștefan Neacşu 15ca24b848 Add more translations from v2 to v3 authenticator (#5091) 2024-11-05 15:30:00 +02:00
Fouad fa551b6d9d Nym node - Fix claim delegator rewards (#5090)
* update function param from mixId to nodeId

* fix claim operator rewards
2024-11-05 13:01:22 +00:00
Bogdan-Ștefan Neacşu c6959d3e2d Make 250 GB/30 days for free ride mode (#5083) 2024-11-05 11:14:43 +02:00
Jędrzej Stuczyński 2569deb080 bugfix: [wallet] displaying delegations for native nymnodes (#5087)
* fixed return type for getting nymnode details

* fixed nym-api queries if using relative paths

* fixed queries for delegations of native nymnodes
2024-11-04 21:15:29 +00:00
379 changed files with 13309 additions and 9882 deletions
@@ -15,24 +15,6 @@ on:
type: boolean
schedule:
- cron: "14 0 * * *"
pull_request:
paths:
- "clients/**"
- "common/**"
- "explorer-api/**"
- "gateway/**"
- "integrations/**"
- "mixnode/**"
- "nym-api/**"
- "nym-node/**"
- "nym-outfox/**"
- 'nym-data-observatory/**'
- "nym-validator-rewarder/**"
- "sdk/rust/nym-sdk/**"
- "service-providers/**"
- "tools/**"
- "nymvisor/**"
- ".github/workflows/ci-build-upload-binaries.yml"
jobs:
publish-nym:
+1 -1
View File
@@ -9,7 +9,7 @@ on:
jobs:
cargo-deny:
runs-on: arc-ubuntu-22.04-dind
runs-on: ubuntu-latest
strategy:
matrix:
checks:
@@ -2,9 +2,5 @@
{
"rust":"stable",
"runOnEvent":"always"
},
{
"rust":"beta",
"runOnEvent":"pull_request"
}
]
+1 -1
View File
@@ -11,7 +11,7 @@ on:
jobs:
check-schema:
name: Generate and check schema
runs-on: arc-ubuntu-20.04
runs-on: ubuntu-20.04
env:
CARGO_TERM_COLOR: always
steps:
@@ -2,11 +2,6 @@ name: ci-contracts-upload-binaries
on:
workflow_dispatch:
pull_request:
paths:
- 'common/**'
- 'contracts/**'
- '.github/workflows/ci-contracts-upload-binaries.yml'
env:
NETWORK: mainnet
+1 -1
View File
@@ -13,7 +13,7 @@ on:
jobs:
matrix_prep:
runs-on: arc-ubuntu-20.04
runs-on: ubuntu-20.04
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
-39
View File
@@ -1,39 +0,0 @@
name: ci-nym-api-tests
on:
workflow_dispatch:
push:
paths:
- "nym-api/**"
defaults:
run:
working-directory: nym-api/tests
jobs:
test:
name: nym-api tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: install yarn in root
run: cd ../.. && yarn install
- name: Install npm
run: npm install
- name: Node v18
uses: actions/setup-node@v4
with:
node-version: 18.1.0
- name: Install yarn
run: yarn install
- name: Run yarn
run: yarn
- name: Run tests
run: yarn test:sandbox
working-directory: nym-api/tests
@@ -55,6 +55,7 @@ jobs:
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Build all binaries
uses: actions-rs/cargo@v1
+4 -3
View File
@@ -14,13 +14,14 @@ jobs:
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
toolchain: 1.77
target: wasm32-unknown-unknown
override: true
components: rustfmt, clippy
- name: Install wasm-opt
run: cargo install --version 0.114.0 wasm-opt
uses: ./.github/actions/install-wasm-opt
with:
version: '114'
- name: Build release contracts
run: make contracts
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.44.3
uses: mikefarah/yq@v4.44.5
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-credential-proxy/Cargo.toml
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.44.3
uses: mikefarah/yq@v4.44.5
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
+14 -18
View File
@@ -25,31 +25,27 @@ jobs:
git config --global user.email "lawrence@nymtech.net"
git config --global user.name "Lawrence Stalder"
- name: Get version from package.json
uses: sergeysova/jq-action@v2
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.44.5
id: get_version
with:
cmd: jq -r '.version' ${{ env.WORKING_DIRECTORY }}/package.json
- name: Check if tag exists
run: |
if git rev-parse ${{ steps.get_version.outputs.value }} >/dev/null 2>&1; then
echo "Tag ${{ steps.get_version.outputs.value }} already exists"
fi
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-network-monitor/Cargo.toml
- name: Remove existing tag if exists
run: |
if git rev-parse ${{ steps.get_version.outputs.value }} >/dev/null 2>&1; then
git push --delete origin ${{ steps.get_version.outputs.value }}
git tag -d ${{ steps.get_version.outputs.value }}
echo "Checking if tag ${{ env.CONTAINER_NAME }}-${{ steps.get_version.outputs.result }} exists..."
if git rev-parse ${{ env.CONTAINER_NAME }}-${{ steps.get_version.outputs.result }} >/dev/null 2>&1; then
echo "Tag ${{ env.CONTAINER_NAME }}-${{ steps.get_version.outputs.result }} already exists"
git push --delete origin ${{ env.CONTAINER_NAME }}-${{ steps.get_version.outputs.result }}
git tag -d ${{ env.CONTAINER_NAME }}-${{ steps.get_version.outputs.result }}
fi
- name: Create tag
run: |
git tag -a ${{ steps.get_version.outputs.value }} -m "Version ${{ steps.get_version.outputs.value }}"
git push origin ${{ steps.get_version.outputs.value }}
git tag -a ${{ env.CONTAINER_NAME }}-${{ steps.get_version.outputs.result }} -m "Version ${{ steps.get_version.outputs.result }}"
git push origin ${{ env.CONTAINER_NAME }}-${{ steps.get_version.outputs.result }}
- name: BuildAndPushImageOnHarbor
run: |
docker build -f nym-network-monitor.dockerfile ${{ env.WORKING_DIRECTORY }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.value }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
docker build -f nym-network-monitor.dockerfile ${{ env.WORKING_DIRECTORY }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
@@ -31,7 +31,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.44.3
uses: mikefarah/yq@v4.44.5
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
@@ -58,4 +58,4 @@ jobs:
- name: BuildAndPushImageOnHarbor
run: |
docker build --build-arg GIT_REF=${{ github.event.inputs.gateway_probe_git_ref }} -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.44.3
uses: mikefarah/yq@v4.44.5
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.44.3
uses: mikefarah/yq@v4.44.5
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.44.3
uses: mikefarah/yq@v4.44.5
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-credential-proxy/Cargo.toml
+192
View File
@@ -4,6 +4,198 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2024.13-magura] (2024-11-18)
- Limit race probability ([#5145])
- bugifx: assign 'node_id' when converting from 'GatewayDetails' to 'TestNode' ([#5143])
- bugfix: make sure to assign correct node_id and identity during 'gateway_details' table migration ([#5142])
- Respond to auth messages with same version ([#5140])
- Pain/polyfill deprecated endpoints ([#5131])
- change: dont select mixnodes bonded with vested tokens into the rewarded set ([#5129])
- nym-credential-proxy-requests: reqwest use rustls-tls ([#5116])
- bugfix: preserve as much as possible of the rewarded set during migration ([#5103])
- Feature/force refresh node ([#5101])
- Add NYM_VPN_API to env files ([#5099])
- bugfix: fixed historical uptimes for nodes ([#5097])
- Remove old use of 1GB constant ([#5096])
- Graceful agent 1.1.5 ([#5093])
- Add more translations from v2 to v3 authenticator ([#5091])
- Nym node - Fix claim delegator rewards ([#5090])
- Make 250 GB/30 days for free ride mode ([#5083])
- Don't increase bandwidth two times ([#5081])
- Fix expiration date as today + 7 days ([#5076])
- Fix gateway decreasing bandwidth ([#5075])
- Allow custom http port to be reset ([#5073])
- bugfix: additional checks inside credential proxy ([#5072])
- chore: deprecated old nym-api client methods and replaced them when possible ([#5069])
- NS API with directory v2 (#5058) ([#5068])
- bugfix: credential-proxy obtain-async ([#5067])
- Allow nym node config updates ([#5066])
- bugfix: use corrext axum extractors for ecash route arguments ([#5065])
- Merge2/release/2024.13 magura ([#5063])
- bugfix/feature: added NymApiClient method to get all skimmed nodes ([#5062])
- Merge1/release/2024.13 magura ([#5061])
- added hacky routes to return nymnodes alongside legacy nodes ([#5051])
- bugfix: mark migrated gateways as rewarded in the previous epoch in case theyre in the rewarded set ([#5049])
- bugfix: adjust runtime storage migration ([#5047])
- bugfix: supersede 'cb13be27f8f61d9ae74d924e85d2e6787895eb14' by using… ([#5046])
- bugfix: restore default http port for nym-api ([#5045])
- bugfix: fix ecash handlers routes ([#5043])
- bugfix: don't assign exit gateways to standby set ([#5041])
- bugfix: make sure nym-nodes are also tested by network monitor ([#5040])
- bugfix: use bonded nym-nodes for determining initial network monitor … ([#5039])
- bugfix: make gateways insert themselves into [local] topology ([#5038])
- Pass poisson flag ([#5037])
- bugfix: use human readable roles for annotations ([#5036])
- bugfix: use old name for 'epoch_role' in SkimmedNode ([#5034])
- bugfix: make sure to use correct highest node id when assigning role ([#5032])
- feature: use axum_client_ip for attempting to extract source ip ([#5031])
- bugfix: fixed backwards incompatibility for /gateways/described endpoint ([#5030])
- bugfix: verifying signed information of legacy nodes ([#5029])
- bugfix: introduce 'LegacyPendingMixNodeChanges' that does not contain 'cost_params_change' ([#5028])
- bugfix: missing #[serde(default)] for announce port ([#5024])
- bugfix: directory v2.1 `get_all_avg_gateway_reliability_in_interval` query ([#5023])
- added 'get_all_described_nodes' to NymApiClient and adjusted return t… ([#5016])
- Reapply fixes to new branch ([#5014])
- Consume only positive bandwidth ([#5013])
- feature: adjusted ticket sizes to the agreed amounts ([#5009])
- Push private ip before inserting ([#5008])
- chore: update itertools in compact ecash ([#4994])
- feature: make accepting t&c a hard requirement for rewarded set selection ([#4993])
- Fix rustfmt in nym-credential-proxy ([#4992])
- bugfix: client memory leak ([#4991])
- Eliminate 0 bandwidth race check ([#4988])
- [DOCs;/operators]: Release notes for v2024.12 aero ([#4984])
- Add topup req constructor ([#4983])
- Fix critical issues SI86 and SI87 from Cure53 ([#4982])
- Rename nym-vpn-api to nym-credential-proxy ([#4981])
- enable global ecash routes even if api is not a signer ([#4980])
- resolve beta clippy issues in contracts ([#4978])
- Re-enable vested delegation migration ([#4977])
- feature: require reporting using nym-node binary for rewarded set selection ([#4976])
- Top up bandwidth ([#4975])
- [Product Data] Add session type based on ecash ticket received ([#4974])
- Bugfix/additional directory fixes ([#4973])
- feat: add Dockerfile for nym node ([#4972])
- chore: remove unused rocket code ([#4968])
- Import nym-vpn-api crates ([#4967])
- feature: importer-cli to correctly handle mixnet/vesting import ([#4966])
- bugfix: fix expected return type on /v1/gateways endpoint ([#4965])
- [Product Data] First step in gateway usage data collection ([#4963])
- Bump sqlx to 0.7.4 ([#4959])
- Add env feature to clap and make clap parameters available as env variables ([#4957])
- Feature/contract state tools ([#4954])
- expose authenticator address along other address in node-details ([#4953])
- Extract packet processing from mixnode-common ([#4949])
- nym-api container ([#4948])
- Ticket type storage ([#4947])
- Add "utoipa" feature to nym-node ([#4945])
- build(deps): bump the patch-updates group across 1 directory with 9 updates ([#4944])
- V2 performance monitoring feature flag ([#4943])
- Bugfix/rewarder post pruning adjustments ([#4942])
- Switch over the last set of jobs to arc runners ([#4938])
- Fix broken build after merge ([#4937])
- bugfix: correctly paginate through 'search_tx' endpoint ([#4936])
- Add more conversions for responses of authenticator messages ([#4929])
- Directory Sevices v2.1 ([#4903])
- Migrate Legacy Node (Frontend) ([#4826])
- Fix critical issues SI84 and SI85 from Cure53 ([#4758])
[#5145]: https://github.com/nymtech/nym/pull/5145
[#5143]: https://github.com/nymtech/nym/pull/5143
[#5142]: https://github.com/nymtech/nym/pull/5142
[#5140]: https://github.com/nymtech/nym/pull/5140
[#5131]: https://github.com/nymtech/nym/pull/5131
[#5129]: https://github.com/nymtech/nym/pull/5129
[#5116]: https://github.com/nymtech/nym/pull/5116
[#5103]: https://github.com/nymtech/nym/pull/5103
[#5101]: https://github.com/nymtech/nym/pull/5101
[#5099]: https://github.com/nymtech/nym/pull/5099
[#5097]: https://github.com/nymtech/nym/pull/5097
[#5096]: https://github.com/nymtech/nym/pull/5096
[#5093]: https://github.com/nymtech/nym/pull/5093
[#5091]: https://github.com/nymtech/nym/pull/5091
[#5090]: https://github.com/nymtech/nym/pull/5090
[#5083]: https://github.com/nymtech/nym/pull/5083
[#5081]: https://github.com/nymtech/nym/pull/5081
[#5076]: https://github.com/nymtech/nym/pull/5076
[#5075]: https://github.com/nymtech/nym/pull/5075
[#5073]: https://github.com/nymtech/nym/pull/5073
[#5072]: https://github.com/nymtech/nym/pull/5072
[#5069]: https://github.com/nymtech/nym/pull/5069
[#5068]: https://github.com/nymtech/nym/pull/5068
[#5067]: https://github.com/nymtech/nym/pull/5067
[#5066]: https://github.com/nymtech/nym/pull/5066
[#5065]: https://github.com/nymtech/nym/pull/5065
[#5063]: https://github.com/nymtech/nym/pull/5063
[#5062]: https://github.com/nymtech/nym/pull/5062
[#5061]: https://github.com/nymtech/nym/pull/5061
[#5051]: https://github.com/nymtech/nym/pull/5051
[#5049]: https://github.com/nymtech/nym/pull/5049
[#5047]: https://github.com/nymtech/nym/pull/5047
[#5046]: https://github.com/nymtech/nym/pull/5046
[#5045]: https://github.com/nymtech/nym/pull/5045
[#5043]: https://github.com/nymtech/nym/pull/5043
[#5041]: https://github.com/nymtech/nym/pull/5041
[#5040]: https://github.com/nymtech/nym/pull/5040
[#5039]: https://github.com/nymtech/nym/pull/5039
[#5038]: https://github.com/nymtech/nym/pull/5038
[#5037]: https://github.com/nymtech/nym/pull/5037
[#5036]: https://github.com/nymtech/nym/pull/5036
[#5034]: https://github.com/nymtech/nym/pull/5034
[#5032]: https://github.com/nymtech/nym/pull/5032
[#5031]: https://github.com/nymtech/nym/pull/5031
[#5030]: https://github.com/nymtech/nym/pull/5030
[#5029]: https://github.com/nymtech/nym/pull/5029
[#5028]: https://github.com/nymtech/nym/pull/5028
[#5024]: https://github.com/nymtech/nym/pull/5024
[#5023]: https://github.com/nymtech/nym/pull/5023
[#5016]: https://github.com/nymtech/nym/pull/5016
[#5014]: https://github.com/nymtech/nym/pull/5014
[#5013]: https://github.com/nymtech/nym/pull/5013
[#5009]: https://github.com/nymtech/nym/pull/5009
[#5008]: https://github.com/nymtech/nym/pull/5008
[#4994]: https://github.com/nymtech/nym/pull/4994
[#4993]: https://github.com/nymtech/nym/pull/4993
[#4992]: https://github.com/nymtech/nym/pull/4992
[#4991]: https://github.com/nymtech/nym/pull/4991
[#4988]: https://github.com/nymtech/nym/pull/4988
[#4984]: https://github.com/nymtech/nym/pull/4984
[#4983]: https://github.com/nymtech/nym/pull/4983
[#4982]: https://github.com/nymtech/nym/pull/4982
[#4981]: https://github.com/nymtech/nym/pull/4981
[#4980]: https://github.com/nymtech/nym/pull/4980
[#4978]: https://github.com/nymtech/nym/pull/4978
[#4977]: https://github.com/nymtech/nym/pull/4977
[#4976]: https://github.com/nymtech/nym/pull/4976
[#4975]: https://github.com/nymtech/nym/pull/4975
[#4974]: https://github.com/nymtech/nym/pull/4974
[#4973]: https://github.com/nymtech/nym/pull/4973
[#4972]: https://github.com/nymtech/nym/pull/4972
[#4968]: https://github.com/nymtech/nym/pull/4968
[#4967]: https://github.com/nymtech/nym/pull/4967
[#4966]: https://github.com/nymtech/nym/pull/4966
[#4965]: https://github.com/nymtech/nym/pull/4965
[#4963]: https://github.com/nymtech/nym/pull/4963
[#4959]: https://github.com/nymtech/nym/pull/4959
[#4957]: https://github.com/nymtech/nym/pull/4957
[#4954]: https://github.com/nymtech/nym/pull/4954
[#4953]: https://github.com/nymtech/nym/pull/4953
[#4949]: https://github.com/nymtech/nym/pull/4949
[#4948]: https://github.com/nymtech/nym/pull/4948
[#4947]: https://github.com/nymtech/nym/pull/4947
[#4945]: https://github.com/nymtech/nym/pull/4945
[#4944]: https://github.com/nymtech/nym/pull/4944
[#4943]: https://github.com/nymtech/nym/pull/4943
[#4942]: https://github.com/nymtech/nym/pull/4942
[#4938]: https://github.com/nymtech/nym/pull/4938
[#4937]: https://github.com/nymtech/nym/pull/4937
[#4936]: https://github.com/nymtech/nym/pull/4936
[#4929]: https://github.com/nymtech/nym/pull/4929
[#4903]: https://github.com/nymtech/nym/pull/4903
[#4826]: https://github.com/nymtech/nym/pull/4826
[#4758]: https://github.com/nymtech/nym/pull/4758
## [2024.12-aero] (2024-10-17)
- nym-node: don't use bloomfilters for double spending checks ([#4960])
Generated
+442 -160
View File
File diff suppressed because it is too large Load Diff
+14 -3
View File
@@ -90,6 +90,7 @@ members = [
"common/statistics",
"common/store-cipher",
"common/task",
"common/ticketbooks-merkle",
"common/topology",
"common/tun",
"common/types",
@@ -148,7 +149,7 @@ members = [
"tools/internal/contract-state-importer/importer-cli",
"tools/internal/contract-state-importer/importer-contract",
"tools/internal/testnet-manager",
"tools/internal/testnet-manager/dkg-bypass-contract",
"tools/internal/testnet-manager/dkg-bypass-contract", "common/nym-tui-common",
]
default-members = [
@@ -277,6 +278,7 @@ ledger-transport = "0.10.0"
ledger-transport-hid = "0.10.0"
log = "0.4"
maxminddb = "0.23.0"
rs_merkle = "1.4.2"
mime = "0.3.17"
moka = { version = "0.12", features = ["future"] }
nix = "0.27.1"
@@ -312,7 +314,7 @@ serde = "1.0.211"
serde_bytes = "0.11.15"
serde_derive = "1.0"
serde_json = "1.0.132"
serde_json_path = "0.6.7"
serde_json_path = "0.7.1"
serde_repr = "0.1"
serde_with = "3.9.0"
serde_yaml = "0.9.25"
@@ -327,7 +329,7 @@ syn = "1"
sysinfo = "0.30.13"
tap = "1.0.1"
tar = "0.4.42"
tempfile = "3.5.0"
tempfile = "3.14"
thiserror = "1.0.64"
time = "0.3.30"
tokio = "1.39"
@@ -410,6 +412,15 @@ wasm-bindgen-futures = "0.4.45"
wasmtimer = "0.2.0"
web-sys = "0.3.72"
# tui related
better-panic = "0.3.0"
color-eyre = "0.6.3"
crossterm = "0.28.1"
strip-ansi-escapes = "0.2.0"
ratatui = "0.29.0"
tui-logger = "0.14.0"
# Profile settings for individual crates
# Compile-time verified queries do quite a bit of work at compile time. Incremental
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.42"
version = "1.1.43"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.42"
version = "1.1.43"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021"
@@ -22,4 +22,7 @@ pub enum Error {
#[error("conversion: {0}")]
Conversion(String),
#[error("failed to serialize response packet: {source}")]
FailedToSerializeResponsePacket { source: Box<bincode::ErrorKind> },
}
+1
View File
@@ -1,6 +1,7 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod traits;
pub mod v1;
pub mod v2;
pub mod v3;
+343
View File
@@ -0,0 +1,343 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_credentials_interface::CredentialSpendingData;
use nym_crypto::asymmetric::x25519::PrivateKey;
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
use nym_sphinx::addressing::clients::Recipient;
use nym_wireguard_types::PeerPublicKey;
use crate::{
v1, v2, v3,
v4::{self, registration::IpPair},
Error,
};
#[derive(Copy, Clone, Debug)]
pub enum AuthenticatorVersion {
V1,
V2,
V3,
V4,
UNKNOWN,
}
impl From<Protocol> for AuthenticatorVersion {
fn from(value: Protocol) -> Self {
if value.service_provider_type != ServiceProviderType::Authenticator {
AuthenticatorVersion::UNKNOWN
} else if value.version == v1::VERSION {
AuthenticatorVersion::V1
} else if value.version == v2::VERSION {
AuthenticatorVersion::V2
} else if value.version == v3::VERSION {
AuthenticatorVersion::V3
} else if value.version == v4::VERSION {
AuthenticatorVersion::V4
} else {
AuthenticatorVersion::UNKNOWN
}
}
}
pub trait InitMessage {
fn pub_key(&self) -> PeerPublicKey;
}
impl InitMessage for v1::registration::InitMessage {
fn pub_key(&self) -> PeerPublicKey {
self.pub_key
}
}
impl InitMessage for v2::registration::InitMessage {
fn pub_key(&self) -> PeerPublicKey {
self.pub_key
}
}
impl InitMessage for v3::registration::InitMessage {
fn pub_key(&self) -> PeerPublicKey {
self.pub_key
}
}
impl InitMessage for v4::registration::InitMessage {
fn pub_key(&self) -> PeerPublicKey {
self.pub_key
}
}
pub trait FinalMessage {
fn pub_key(&self) -> PeerPublicKey;
fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error>;
fn private_ips(&self) -> IpPair;
fn credential(&self) -> Option<CredentialSpendingData>;
}
impl FinalMessage for v1::GatewayClient {
fn pub_key(&self) -> PeerPublicKey {
self.pub_key
}
fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
self.verify(private_key, nonce)
}
fn private_ips(&self) -> IpPair {
self.private_ip.into()
}
fn credential(&self) -> Option<CredentialSpendingData> {
None
}
}
impl FinalMessage for v2::registration::FinalMessage {
fn pub_key(&self) -> PeerPublicKey {
self.gateway_client.pub_key
}
fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
self.gateway_client.verify(private_key, nonce)
}
fn private_ips(&self) -> IpPair {
self.gateway_client.private_ip.into()
}
fn credential(&self) -> Option<CredentialSpendingData> {
self.credential.clone()
}
}
impl FinalMessage for v3::registration::FinalMessage {
fn pub_key(&self) -> PeerPublicKey {
self.gateway_client.pub_key
}
fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
self.gateway_client.verify(private_key, nonce)
}
fn private_ips(&self) -> IpPair {
self.gateway_client.private_ip.into()
}
fn credential(&self) -> Option<CredentialSpendingData> {
self.credential.clone()
}
}
impl FinalMessage for v4::registration::FinalMessage {
fn pub_key(&self) -> PeerPublicKey {
self.gateway_client.pub_key
}
fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
self.gateway_client.verify(private_key, nonce)
}
fn private_ips(&self) -> IpPair {
self.gateway_client.private_ips
}
fn credential(&self) -> Option<CredentialSpendingData> {
self.credential.clone()
}
}
pub trait QueryBandwidthMessage {
fn pub_key(&self) -> PeerPublicKey;
}
impl QueryBandwidthMessage for PeerPublicKey {
fn pub_key(&self) -> PeerPublicKey {
*self
}
}
pub trait TopUpMessage {
fn pub_key(&self) -> PeerPublicKey;
fn credential(&self) -> CredentialSpendingData;
}
impl TopUpMessage for v3::topup::TopUpMessage {
fn pub_key(&self) -> PeerPublicKey {
self.pub_key
}
fn credential(&self) -> CredentialSpendingData {
self.credential.clone()
}
}
impl TopUpMessage for v4::topup::TopUpMessage {
fn pub_key(&self) -> PeerPublicKey {
self.pub_key
}
fn credential(&self) -> CredentialSpendingData {
self.credential.clone()
}
}
pub enum AuthenticatorRequest {
Initial {
msg: Box<dyn InitMessage + Send + Sync + 'static>,
protocol: Protocol,
reply_to: Recipient,
request_id: u64,
},
Final {
msg: Box<dyn FinalMessage + Send + Sync + 'static>,
protocol: Protocol,
reply_to: Recipient,
request_id: u64,
},
QueryBandwidth {
msg: Box<dyn QueryBandwidthMessage + Send + Sync + 'static>,
protocol: Protocol,
reply_to: Recipient,
request_id: u64,
},
TopUpBandwidth {
msg: Box<dyn TopUpMessage + Send + Sync + 'static>,
protocol: Protocol,
reply_to: Recipient,
request_id: u64,
},
}
impl From<v1::request::AuthenticatorRequest> for AuthenticatorRequest {
fn from(value: v1::request::AuthenticatorRequest) -> Self {
match value.data {
v1::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: Protocol {
version: value.version,
service_provider_type: ServiceProviderType::Authenticator,
},
reply_to: value.reply_to,
request_id: value.request_id,
},
v1::request::AuthenticatorRequestData::Final(gateway_client) => Self::Final {
msg: Box::new(gateway_client),
protocol: Protocol {
version: value.version,
service_provider_type: ServiceProviderType::Authenticator,
},
reply_to: value.reply_to,
request_id: value.request_id,
},
v1::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: Protocol {
version: value.version,
service_provider_type: ServiceProviderType::Authenticator,
},
reply_to: value.reply_to,
request_id: value.request_id,
}
}
}
}
}
impl From<v2::request::AuthenticatorRequest> for AuthenticatorRequest {
fn from(value: v2::request::AuthenticatorRequest) -> Self {
match value.data {
v2::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: value.protocol,
reply_to: value.reply_to,
request_id: value.request_id,
},
v2::request::AuthenticatorRequestData::Final(final_message) => Self::Final {
msg: final_message,
protocol: value.protocol,
reply_to: value.reply_to,
request_id: value.request_id,
},
v2::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: value.protocol,
reply_to: value.reply_to,
request_id: value.request_id,
}
}
}
}
}
impl From<v3::request::AuthenticatorRequest> for AuthenticatorRequest {
fn from(value: v3::request::AuthenticatorRequest) -> Self {
match value.data {
v3::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: value.protocol,
reply_to: value.reply_to,
request_id: value.request_id,
},
v3::request::AuthenticatorRequestData::Final(final_message) => Self::Final {
msg: final_message,
protocol: value.protocol,
reply_to: value.reply_to,
request_id: value.request_id,
},
v3::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: value.protocol,
reply_to: value.reply_to,
request_id: value.request_id,
}
}
v3::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => {
Self::TopUpBandwidth {
msg: top_up_message,
protocol: value.protocol,
reply_to: value.reply_to,
request_id: value.request_id,
}
}
}
}
}
impl From<v4::request::AuthenticatorRequest> for AuthenticatorRequest {
fn from(value: v4::request::AuthenticatorRequest) -> Self {
match value.data {
v4::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: value.protocol,
reply_to: value.reply_to,
request_id: value.request_id,
},
v4::request::AuthenticatorRequestData::Final(final_message) => Self::Final {
msg: final_message,
protocol: value.protocol,
reply_to: value.reply_to,
request_id: value.request_id,
},
v4::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: value.protocol,
reply_to: value.reply_to,
request_id: value.request_id,
}
}
v4::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => {
Self::TopUpBandwidth {
msg: top_up_message,
protocol: value.protocol,
reply_to: value.reply_to,
request_id: value.request_id,
}
}
}
}
}
@@ -98,6 +98,16 @@ impl TryFrom<v3::response::AuthenticatorResponse> for v2::response::Authenticato
}
}
impl From<v2::response::AuthenticatorResponse> for v3::response::AuthenticatorResponse {
fn from(value: v2::response::AuthenticatorResponse) -> Self {
Self {
protocol: value.protocol,
data: value.data.into(),
reply_to: value.reply_to,
}
}
}
impl TryFrom<v3::response::AuthenticatorResponseData> for v2::response::AuthenticatorResponseData {
type Error = crate::Error;
@@ -129,6 +139,22 @@ impl TryFrom<v3::response::AuthenticatorResponseData> for v2::response::Authenti
}
}
impl From<v2::response::AuthenticatorResponseData> for v3::response::AuthenticatorResponseData {
fn from(value: v2::response::AuthenticatorResponseData) -> Self {
match value {
v2::response::AuthenticatorResponseData::PendingRegistration(
pending_registration_response,
) => Self::PendingRegistration(pending_registration_response.into()),
v2::response::AuthenticatorResponseData::Registered(registered_response) => {
Self::Registered(registered_response.into())
}
v2::response::AuthenticatorResponseData::RemainingBandwidth(
remaining_bandwidth_response,
) => Self::RemainingBandwidth(remaining_bandwidth_response.into()),
}
}
}
impl From<v3::response::PendingRegistrationResponse> for v2::response::PendingRegistrationResponse {
fn from(value: v3::response::PendingRegistrationResponse) -> Self {
Self {
@@ -139,6 +165,16 @@ impl From<v3::response::PendingRegistrationResponse> for v2::response::PendingRe
}
}
impl From<v2::response::PendingRegistrationResponse> for v3::response::PendingRegistrationResponse {
fn from(value: v2::response::PendingRegistrationResponse) -> Self {
Self {
request_id: value.request_id,
reply_to: value.reply_to,
reply: value.reply.into(),
}
}
}
impl From<v3::response::RegisteredResponse> for v2::response::RegisteredResponse {
fn from(value: v3::response::RegisteredResponse) -> Self {
Self {
@@ -149,6 +185,16 @@ impl From<v3::response::RegisteredResponse> for v2::response::RegisteredResponse
}
}
impl From<v2::response::RegisteredResponse> for v3::response::RegisteredResponse {
fn from(value: v2::response::RegisteredResponse) -> Self {
Self {
request_id: value.request_id,
reply_to: value.reply_to,
reply: value.reply.into(),
}
}
}
impl From<v3::response::RemainingBandwidthResponse> for v2::response::RemainingBandwidthResponse {
fn from(value: v3::response::RemainingBandwidthResponse) -> Self {
Self {
@@ -159,6 +205,16 @@ impl From<v3::response::RemainingBandwidthResponse> for v2::response::RemainingB
}
}
impl From<v2::response::RemainingBandwidthResponse> for v3::response::RemainingBandwidthResponse {
fn from(value: v2::response::RemainingBandwidthResponse) -> Self {
Self {
request_id: value.request_id,
reply_to: value.reply_to,
reply: value.reply.map(Into::into),
}
}
}
impl From<v3::registration::RegistrationData> for v2::registration::RegistrationData {
fn from(value: v3::registration::RegistrationData) -> Self {
Self {
@@ -169,6 +225,16 @@ impl From<v3::registration::RegistrationData> for v2::registration::Registration
}
}
impl From<v2::registration::RegistrationData> for v3::registration::RegistrationData {
fn from(value: v2::registration::RegistrationData) -> Self {
Self {
nonce: value.nonce,
gateway_data: value.gateway_data.into(),
wg_port: value.wg_port,
}
}
}
impl From<v3::registration::RegistredData> for v2::registration::RegistredData {
fn from(value: v3::registration::RegistredData) -> Self {
Self {
@@ -179,6 +245,16 @@ impl From<v3::registration::RegistredData> for v2::registration::RegistredData {
}
}
impl From<v2::registration::RegistredData> for v3::registration::RegistredData {
fn from(value: v2::registration::RegistredData) -> Self {
Self {
pub_key: value.pub_key,
private_ip: value.private_ip,
wg_port: value.wg_port,
}
}
}
impl From<v3::registration::RemainingBandwidthData> for v2::registration::RemainingBandwidthData {
fn from(value: v3::registration::RemainingBandwidthData) -> Self {
Self {
@@ -186,3 +262,11 @@ impl From<v3::registration::RemainingBandwidthData> for v2::registration::Remain
}
}
}
impl From<v2::registration::RemainingBandwidthData> for v3::registration::RemainingBandwidthData {
fn from(value: v2::registration::RemainingBandwidthData) -> Self {
Self {
available_bandwidth: value.available_bandwidth,
}
}
}
@@ -27,7 +27,7 @@ pub type HmacSha256 = Hmac<Sha256>;
pub type Nonce = u64;
pub type Taken = Option<SystemTime>;
pub const BANDWIDTH_CAP_PER_DAY: u64 = 1024 * 1024 * 1024; // 1 GB
pub const BANDWIDTH_CAP_PER_DAY: u64 = 250 * 1024 * 1024 * 1024; // 250 GB
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct InitMessage {
@@ -3,7 +3,7 @@
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
use crate::{v3, v4};
use crate::{v2, v3, v4};
impl From<v3::request::AuthenticatorRequest> for v4::request::AuthenticatorRequest {
fn from(authenticator_request: v3::request::AuthenticatorRequest) -> Self {
@@ -64,6 +64,16 @@ impl From<Box<v3::topup::TopUpMessage>> for Box<v4::topup::TopUpMessage> {
}
}
impl From<v2::registration::GatewayClient> for v4::registration::GatewayClient {
fn from(gw_client: v2::registration::GatewayClient) -> Self {
Self {
pub_key: gw_client.pub_key,
private_ips: gw_client.private_ip.into(),
mac: gw_client.mac.into(),
}
}
}
impl From<v3::registration::GatewayClient> for v4::registration::GatewayClient {
fn from(gw_client: v3::registration::GatewayClient) -> Self {
Self {
@@ -84,6 +94,22 @@ impl From<v4::registration::GatewayClient> for v3::registration::GatewayClient {
}
}
impl From<v4::registration::GatewayClient> for v2::registration::GatewayClient {
fn from(gw_client: v4::registration::GatewayClient) -> Self {
Self {
pub_key: gw_client.pub_key,
private_ip: gw_client.private_ips.ipv4.into(),
mac: gw_client.mac.into(),
}
}
}
impl From<v2::registration::ClientMac> for v4::registration::ClientMac {
fn from(mac: v2::registration::ClientMac) -> Self {
Self::new(mac.to_vec())
}
}
impl From<v3::registration::ClientMac> for v4::registration::ClientMac {
fn from(mac: v3::registration::ClientMac) -> Self {
Self::new(mac.to_vec())
@@ -96,6 +122,12 @@ impl From<v4::registration::ClientMac> for v3::registration::ClientMac {
}
}
impl From<v4::registration::ClientMac> for v2::registration::ClientMac {
fn from(mac: v4::registration::ClientMac) -> Self {
Self::new(mac.to_vec())
}
}
impl TryFrom<v4::response::AuthenticatorResponse> for v3::response::AuthenticatorResponse {
type Error = crate::Error;
@@ -42,6 +42,12 @@ impl IpPair {
}
}
impl From<(Ipv4Addr, Ipv6Addr)> for IpPair {
fn from((ipv4, ipv6): (Ipv4Addr, Ipv6Addr)) -> Self {
IpPair { ipv4, ipv6 }
}
}
impl fmt::Display for IpPair {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {})", self.ipv4, self.ipv6)
+1
View File
@@ -47,6 +47,7 @@ pub fn setup_logging() {
#[cfg(feature = "basic_tracing")]
pub fn setup_tracing_logger() {
let log_builder = tracing_subscriber::fmt()
.with_writer(std::io::stderr)
// Use a more compact, abbreviated log format
.compact()
// Display source code file paths
@@ -9,7 +9,6 @@ license.workspace = true
[dependencies]
humantime-serde = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_with = { workspace = true, features = ["macros"] }
thiserror.workspace = true
url = { workspace = true, features = ["serde"] }
+5 -3
View File
@@ -2,10 +2,10 @@
// SPDX-License-Identifier: Apache-2.0
use nym_config::defaults::NymNetworkDetails;
use nym_config::serde_helpers::{de_maybe_stringified, ser_maybe_stringified};
use nym_sphinx_addressing::Recipient;
use nym_sphinx_params::{PacketSize, PacketType};
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr};
use std::time::Duration;
use url::Url;
@@ -643,7 +643,6 @@ impl Default for ReplySurbs {
}
}
#[serde_as]
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct StatsReporting {
@@ -651,7 +650,10 @@ pub struct StatsReporting {
pub enabled: bool,
/// Address of the stats collector. If this is none, no reporting will happen, regardless of `enabled`
#[serde_as(as = "Option<DisplayFromStr>")]
#[serde(
serialize_with = "ser_maybe_stringified",
deserialize_with = "de_maybe_stringified"
)]
pub provider_address: Option<Recipient>,
/// With what frequence will statistics be sent
@@ -49,13 +49,13 @@ use nym_sphinx::addressing::nodes::NodeIdentity;
use nym_sphinx::params::PacketType;
use nym_sphinx::receiver::{ReconstructedMessage, SphinxMessageReceiver};
use nym_statistics_common::clients::ClientStatsSender;
use nym_statistics_common::generate_client_stats_id;
use nym_task::connections::{ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths};
use nym_task::{TaskClient, TaskHandle};
use nym_topology::provider_trait::TopologyProvider;
use nym_topology::HardcodedTopologyProvider;
use nym_validator_client::{nyxd::contract_traits::DkgQueryClient, UserAgent};
use rand::rngs::OsRng;
use sha2::Digest;
use std::fmt::Debug;
use std::os::raw::c_int as RawFd;
use std::path::Path;
@@ -358,6 +358,7 @@ where
bandwidth_controller: Option<BandwidthController<C, S::CredentialStore>>,
details_store: &S::GatewaysDetailsStore,
packet_router: PacketRouter,
stats_reporter: ClientStatsSender,
shutdown: TaskClient,
) -> Result<GatewayClient<C, S::CredentialStore>, ClientCoreError>
where
@@ -373,7 +374,12 @@ where
let mut gateway_client =
if let Some(existing_client) = initialisation_result.authenticated_ephemeral_client {
existing_client.upgrade(packet_router, bandwidth_controller, shutdown)
existing_client.upgrade(
packet_router,
bandwidth_controller,
stats_reporter,
shutdown,
)
} else {
let cfg = GatewayConfig::new(
details.gateway_id,
@@ -394,6 +400,7 @@ where
Some(details.shared_key),
packet_router,
bandwidth_controller,
stats_reporter,
shutdown,
)
};
@@ -446,6 +453,7 @@ where
Ok(gateway_client)
}
#[allow(clippy::too_many_arguments)]
async fn setup_gateway_transceiver(
custom_gateway_transceiver: Option<Box<dyn GatewayTransceiver + Send>>,
config: &Config,
@@ -453,6 +461,7 @@ where
bandwidth_controller: Option<BandwidthController<C, S::CredentialStore>>,
details_store: &S::GatewaysDetailsStore,
packet_router: PacketRouter,
stats_reporter: ClientStatsSender,
mut shutdown: TaskClient,
) -> Result<Box<dyn GatewayTransceiver + Send>, ClientCoreError>
where
@@ -483,6 +492,7 @@ where
bandwidth_controller,
details_store,
packet_router,
stats_reporter,
shutdown,
)
.await?;
@@ -734,15 +744,10 @@ where
self.user_agent.clone(),
);
//make sure we don't accidentally get the same id as gateways are reporting
let client_stats_id = format!(
"stats_id_{:x}",
sha2::Sha256::digest(self_address.identity().to_bytes())
);
let stats_reporter = Self::start_statistics_control(
self.config,
self.user_agent.clone(),
client_stats_id,
generate_client_stats_id(*self_address.identity()),
input_sender.clone(),
shutdown.fork("statistics_control"),
);
@@ -771,6 +776,7 @@ where
bandwidth_controller,
&details_store,
gateway_packet_router,
stats_reporter.clone(),
shutdown.fork("gateway_transceiver"),
)
.await?;
@@ -29,6 +29,7 @@ nym-crypto = { path = "../../crypto" }
nym-gateway-requests = { path = "../../gateway-requests" }
nym-network-defaults = { path = "../../network-defaults" }
nym-sphinx = { path = "../../nymsphinx" }
nym-statistics-common = { path = "../../statistics" }
nym-pemstore = { path = "../../pemstore" }
nym-validator-client = { path = "../validator-client", default-features = false }
nym-task = { path = "../../task" }
@@ -25,6 +25,8 @@ use nym_gateway_requests::{
CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION,
};
use nym_sphinx::forwarding::packet::MixPacket;
use nym_statistics_common::clients::connection::ConnectionStatsEvent;
use nym_statistics_common::clients::ClientStatsSender;
use nym_task::TaskClient;
use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
use rand::rngs::OsRng;
@@ -94,6 +96,7 @@ pub struct GatewayClient<C, St = EphemeralCredentialStorage> {
connection: SocketState,
packet_router: PacketRouter,
bandwidth_controller: Option<BandwidthController<C, St>>,
stats_reporter: ClientStatsSender,
// currently unused (but populated)
negotiated_protocol: Option<u8>,
@@ -103,6 +106,7 @@ pub struct GatewayClient<C, St = EphemeralCredentialStorage> {
}
impl<C, St> GatewayClient<C, St> {
#[allow(clippy::too_many_arguments)]
pub fn new(
cfg: GatewayClientConfig,
gateway_config: GatewayConfig,
@@ -111,6 +115,7 @@ impl<C, St> GatewayClient<C, St> {
shared_key: Option<Arc<SharedGatewayKey>>,
packet_router: PacketRouter,
bandwidth_controller: Option<BandwidthController<C, St>>,
stats_reporter: ClientStatsSender,
task_client: TaskClient,
) -> Self {
GatewayClient {
@@ -124,6 +129,7 @@ impl<C, St> GatewayClient<C, St> {
connection: SocketState::NotConnected,
packet_router,
bandwidth_controller,
stats_reporter,
negotiated_protocol: None,
task_client,
}
@@ -714,6 +720,7 @@ impl<C, St> GatewayClient<C, St> {
{
// TODO: make it configurable
const TICKETS_TO_SPEND: u32 = 1;
const MIXNET_TICKET: TicketType = TicketType::V1MixnetEntry;
if !self.authenticated {
return Err(GatewayClientError::NotAuthenticated);
@@ -750,14 +757,23 @@ impl<C, St> GatewayClient<C, St> {
let prepared_credential = self
.unchecked_bandwidth_controller()
.prepare_ecash_ticket(
TicketType::V1MixnetEntry,
MIXNET_TICKET,
self.gateway_identity.to_bytes(),
TICKETS_TO_SPEND,
)
.await?;
match self.claim_ecash_bandwidth(prepared_credential.data).await {
Ok(_) => Ok(()),
Ok(_) => {
self.stats_reporter.report(
ConnectionStatsEvent::TicketSpent {
typ: MIXNET_TICKET,
amount: TICKETS_TO_SPEND,
}
.into(),
);
Ok(())
}
Err(err) => {
error!("failed to claim ecash bandwidth with the gateway...: {err}");
if err.is_ticket_replay() {
@@ -1030,6 +1046,7 @@ impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
connection: SocketState::NotConnected,
packet_router,
bandwidth_controller: None,
stats_reporter: ClientStatsSender::new(None),
negotiated_protocol: None,
task_client,
}
@@ -1039,6 +1056,7 @@ impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
self,
packet_router: PacketRouter,
bandwidth_controller: Option<BandwidthController<C, St>>,
stats_reporter: ClientStatsSender,
task_client: TaskClient,
) -> GatewayClient<C, St> {
// invariants that can't be broken
@@ -1058,6 +1076,7 @@ impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
connection: self.connection,
packet_router,
bandwidth_controller,
stats_reporter,
negotiated_protocol: self.negotiated_protocol,
task_client,
}
@@ -11,7 +11,8 @@ use crate::{
use nym_api_requests::ecash::models::{
AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse,
BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashTicketVerificationResponse,
SpentCredentialsResponse, VerifyEcashTicketBody,
IssuedTicketbooksChallengeResponse, IssuedTicketbooksForResponse, SpentCredentialsResponse,
VerifyEcashTicketBody,
};
use nym_api_requests::ecash::{
BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse,
@@ -24,16 +25,17 @@ use nym_api_requests::models::{
use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated};
use nym_api_requests::nym_nodes::SkimmedNode;
use nym_coconut_dkg_common::types::EpochId;
use nym_ecash_contract_common::deposit::DepositId;
use nym_http_api_client::UserAgent;
use nym_network_defaults::NymNetworkDetails;
use time::Date;
use url::Url;
pub use crate::nym_api::NymApiClientExt;
use nym_mixnet_contract_common::NymNodeDetails;
pub use nym_mixnet_contract_common::{
mixnode::MixNodeDetails, GatewayBond, IdentityKey, IdentityKeyRef, NodeId,
mixnode::MixNodeDetails, GatewayBond, IdentityKey, IdentityKeyRef, NodeId, NymNodeDetails,
};
// re-export the type to not break existing imports
pub use crate::coconut::EcashApiClient;
@@ -330,10 +332,10 @@ impl NymApiClient {
NymApiClient { nym_api }
}
pub fn new_with_user_agent(api_url: Url, user_agent: UserAgent) -> Self {
pub fn new_with_user_agent(api_url: Url, user_agent: impl Into<UserAgent>) -> Self {
let nym_api = nym_api::Client::builder::<_, ValidatorClientError>(api_url)
.expect("invalid api url")
.with_user_agent(user_agent)
.with_user_agent(user_agent.into())
.build::<ValidatorClientError>()
.expect("failed to build nym api client");
@@ -696,4 +698,22 @@ impl NymApiClient {
) -> Result<VerificationKeyResponse, ValidatorClientError> {
Ok(self.nym_api.master_verification_key(epoch_id).await?)
}
pub async fn issued_ticketbooks_for(
&self,
expiration_date: Date,
) -> Result<IssuedTicketbooksForResponse, ValidatorClientError> {
Ok(self.nym_api.issued_ticketbooks_for(expiration_date).await?)
}
pub async fn issued_ticketbooks_challenge(
&self,
expiration_date: Date,
deposits: Vec<DepositId>,
) -> Result<IssuedTicketbooksChallengeResponse, ValidatorClientError> {
Ok(self
.nym_api
.issued_ticketbooks_challenge(expiration_date, deposits)
.await?)
}
}
@@ -7,36 +7,34 @@ use async_trait::async_trait;
use nym_api_requests::ecash::models::{
AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse,
BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashTicketVerificationResponse,
VerifyEcashTicketBody,
IssuedTicketbooksChallengeRequest, IssuedTicketbooksChallengeResponse,
IssuedTicketbooksForResponse, VerifyEcashTicketBody,
};
use nym_api_requests::ecash::VerificationKeyResponse;
use nym_api_requests::models::{
AnnotationResponse, ApiHealthResponse, LegacyDescribedMixNode, NodePerformanceResponse,
NymNodeDescription,
NodeRefreshBody, NymNodeDescription,
};
use nym_api_requests::nym_nodes::PaginatedCachedNodesResponse;
use nym_api_requests::pagination::PaginatedResponse;
pub use nym_api_requests::{
ecash::{
models::{
EpochCredentialsResponse, IssuedCredentialResponse, IssuedCredentialsResponse,
IssuedTicketbook, IssuedTicketbookBody, SpentCredentialsResponse,
},
BlindSignRequestBody, BlindedSignatureResponse, CredentialsRequestBody,
models::SpentCredentialsResponse, BlindSignRequestBody, BlindedSignatureResponse,
PartialCoinIndicesSignatureResponse, PartialExpirationDateSignatureResponse,
VerifyEcashCredentialBody,
},
models::{
ComputeRewardEstParam, GatewayBondAnnotated, GatewayCoreStatusResponse,
GatewayStatusReportResponse, GatewayUptimeHistoryResponse, InclusionProbabilityResponse,
LegacyDescribedGateway, MixNodeBondAnnotated, MixnodeCoreStatusResponse,
MixnodeStatusReportResponse, MixnodeStatusResponse, MixnodeUptimeHistoryResponse,
RewardEstimationResponse, StakeSaturationResponse, UptimeResponse,
GatewayStatusReportResponse, GatewayUptimeHistoryResponse, LegacyDescribedGateway,
MixNodeBondAnnotated, MixnodeCoreStatusResponse, MixnodeStatusReportResponse,
MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse,
StakeSaturationResponse, UptimeResponse,
},
nym_nodes::{CachedNodesResponse, SkimmedNode},
};
pub use nym_coconut_dkg_common::types::EpochId;
use nym_contracts_common::IdentityKey;
use nym_ecash_contract_common::deposit::DepositId;
pub use nym_http_api_client::Client;
use nym_http_api_client::{ApiClient, NO_PARAMS};
use nym_mixnet_contract_common::mixnode::MixNodeDetails;
@@ -673,11 +671,12 @@ pub trait NymApiClientExt: ApiClient {
}
#[deprecated]
#[allow(deprecated)]
#[instrument(level = "debug", skip(self))]
async fn get_mixnode_inclusion_probability(
&self,
mix_id: NodeId,
) -> Result<InclusionProbabilityResponse, NymAPIError> {
) -> Result<nym_api_requests::models::InclusionProbabilityResponse, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
@@ -696,16 +695,32 @@ pub trait NymApiClientExt: ApiClient {
&self,
node_id: NodeId,
) -> Result<NodePerformanceResponse, NymAPIError> {
self.get_json_from(format!("/v1/nym-nodes/performance/{node_id}"))
.await
self.get_json(
&[
routes::API_VERSION,
"nym-nodes",
"performance",
&node_id.to_string(),
],
NO_PARAMS,
)
.await
}
async fn get_node_annotation(
&self,
node_id: NodeId,
) -> Result<AnnotationResponse, NymAPIError> {
self.get_json_from(format!("/v1/nym-nodes/annotation/{node_id}"))
.await
self.get_json(
&[
routes::API_VERSION,
"nym-nodes",
"annotation",
&node_id.to_string(),
],
NO_PARAMS,
)
.await
}
#[deprecated]
@@ -911,62 +926,56 @@ pub trait NymApiClientExt: ApiClient {
&[
routes::API_VERSION,
routes::ECASH_ROUTES,
routes::ecash::MASTER_VERIFICATION_KEY,
ecash::MASTER_VERIFICATION_KEY,
],
&params,
)
.await
}
#[instrument(level = "debug", skip(self))]
async fn epoch_credentials(
async fn force_refresh_describe_cache(
&self,
dkg_epoch: EpochId,
) -> Result<EpochCredentialsResponse, NymAPIError> {
request: &NodeRefreshBody,
) -> Result<(), NymAPIError> {
self.post_json(
&[routes::API_VERSION, "nym-nodes", "refresh-described"],
NO_PARAMS,
request,
)
.await
}
async fn issued_ticketbooks_for(
&self,
expiration_date: Date,
) -> Result<IssuedTicketbooksForResponse, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::ECASH_ROUTES,
routes::ECASH_EPOCH_CREDENTIALS,
&dkg_epoch.to_string(),
routes::ECASH_ISSUED_TICKETBOOKS_FOR,
&expiration_date.to_string(),
],
NO_PARAMS,
)
.await
}
#[instrument(level = "debug", skip(self))]
async fn issued_credential(
async fn issued_ticketbooks_challenge(
&self,
credential_id: i64,
) -> Result<IssuedCredentialResponse, NymAPIError> {
self.get_json(
&[
routes::API_VERSION,
routes::ECASH_ROUTES,
routes::ECASH_ISSUED_CREDENTIAL,
&credential_id.to_string(),
],
NO_PARAMS,
)
.await
}
#[instrument(level = "debug", skip(self))]
async fn issued_credentials(
&self,
credential_ids: Vec<i64>,
) -> Result<IssuedCredentialsResponse, NymAPIError> {
expiration_date: Date,
deposits: Vec<DepositId>,
) -> Result<IssuedTicketbooksChallengeResponse, NymAPIError> {
self.post_json(
&[
routes::API_VERSION,
routes::ECASH_ROUTES,
routes::ECASH_ISSUED_CREDENTIALS,
routes::ECASH_ISSUED_TICKETBOOKS_CHALLENGE,
],
NO_PARAMS,
&CredentialsRequestBody {
credential_ids,
pagination: None,
&IssuedTicketbooksChallengeRequest {
expiration_date,
deposits,
},
)
.await
@@ -27,9 +27,8 @@ pub mod ecash {
pub const PARTIAL_COIN_INDICES_SIGNATURES: &str = "partial-coin-indices-signatures";
pub const GLOBAL_COIN_INDICES_SIGNATURES: &str = "aggregated-coin-indices-signatures";
pub const MASTER_VERIFICATION_KEY: &str = "master-verification-key";
pub const ECASH_EPOCH_CREDENTIALS: &str = "epoch-credentials";
pub const ECASH_ISSUED_CREDENTIAL: &str = "issued-credential";
pub const ECASH_ISSUED_CREDENTIALS: &str = "issued-credentials";
pub const ECASH_ISSUED_TICKETBOOKS_FOR: &str = "issued-ticketbooks-for";
pub const ECASH_ISSUED_TICKETBOOKS_CHALLENGE: &str = "issued-ticketbooks-challenge";
pub const EXPIRATION_DATE_PARAM: &str = "expiration_date";
pub const EPOCH_ID_PARAM: &str = "epoch_id";
@@ -7,6 +7,7 @@ use crate::nyxd::error::NyxdError;
use crate::nyxd::CosmWasmClient;
use async_trait::async_trait;
use cosmwasm_std::Coin;
use nym_ecash_contract_common::deposit::LatestDepositResponse;
use nym_ecash_contract_common::msg::QueryMsg as EcashQueryMsg;
use serde::Deserialize;
@@ -51,6 +52,11 @@ pub trait EcashQueryClient {
.await
}
async fn get_latest_deposit(&self) -> Result<LatestDepositResponse, NyxdError> {
self.query_ecash_contract(EcashQueryMsg::GetLatestDeposit {})
.await
}
async fn get_deposits_paged(
&self,
start_after: Option<u32>,
@@ -98,7 +104,6 @@ where
mod tests {
use super::*;
use crate::nyxd::contract_traits::tests::IgnoreValue;
use nym_ecash_contract_common::msg::QueryMsg;
// it's enough that this compiles and clippy is happy about it
#[allow(dead_code)]
@@ -110,14 +115,17 @@ mod tests {
EcashQueryMsg::GetBlacklistedAccount { public_key } => {
client.get_blacklisted_account(public_key).ignore()
}
QueryMsg::GetBlacklistPaged { limit, start_after } => {
EcashQueryMsg::GetBlacklistPaged { limit, start_after } => {
client.get_blacklist_paged(start_after, limit).ignore()
}
QueryMsg::GetDeposit { deposit_id } => client.get_deposit(deposit_id).ignore(),
QueryMsg::GetDepositsPaged { limit, start_after } => {
EcashQueryMsg::GetDeposit { deposit_id } => client.get_deposit(deposit_id).ignore(),
EcashQueryMsg::GetDepositsPaged { limit, start_after } => {
client.get_deposits_paged(start_after, limit).ignore()
}
QueryMsg::GetRequiredDepositAmount {} => client.get_required_deposit_amount().ignore(),
EcashQueryMsg::GetRequiredDepositAmount {} => {
client.get_required_deposit_amount().ignore()
}
EcashQueryMsg::GetLatestDeposit {} => client.get_latest_deposit().ignore(),
};
}
}
@@ -10,10 +10,10 @@ use cosmrs::AccountId;
use nym_contracts_common::signing::Nonce;
use nym_mixnet_contract_common::gateway::{PreassignedGatewayIdsResponse, PreassignedId};
use nym_mixnet_contract_common::nym_node::{
EpochAssignmentResponse, NodeDetailsByIdentityResponse, NodeOwnershipResponse,
NodeRewardingDetailsResponse, PagedNymNodeBondsResponse, PagedNymNodeDetailsResponse,
PagedUnbondedNymNodesResponse, Role, RolesMetadataResponse, StakeSaturationResponse,
UnbondedNodeResponse, UnbondedNymNode,
EpochAssignmentResponse, NodeDetailsByIdentityResponse, NodeDetailsResponse,
NodeOwnershipResponse, NodeRewardingDetailsResponse, PagedNymNodeBondsResponse,
PagedNymNodeDetailsResponse, PagedUnbondedNymNodesResponse, Role, RolesMetadataResponse,
StakeSaturationResponse, UnbondedNodeResponse, UnbondedNymNode,
};
use nym_mixnet_contract_common::reward_params::WorkFactor;
use nym_mixnet_contract_common::{
@@ -66,11 +66,6 @@ pub trait MixnetQueryClient {
.await
}
async fn get_mixnet_contract_settings(&self) -> Result<ContractStateParams, NyxdError> {
self.query_mixnet_contract(MixnetQueryMsg::GetStateParams {})
.await
}
async fn get_mixnet_contract_state_params(&self) -> Result<ContractStateParams, NyxdError> {
self.query_mixnet_contract(MixnetQueryMsg::GetStateParams {})
.await
@@ -316,10 +311,7 @@ pub trait MixnetQueryClient {
.await
}
async fn get_nymnode_details(
&self,
node_id: NodeId,
) -> Result<NodeOwnershipResponse, NyxdError> {
async fn get_nymnode_details(&self, node_id: NodeId) -> Result<NodeDetailsResponse, NyxdError> {
self.query_mixnet_contract(MixnetQueryMsg::GetNymNodeDetails { node_id })
.await
}
@@ -17,7 +17,7 @@ use nym_mixnet_contract_common::reward_params::{
ActiveSetUpdate, IntervalRewardingParamsUpdate, NodeRewardingParameters,
};
use nym_mixnet_contract_common::{
ContractStateParams, ExecuteMsg as MixnetExecuteMsg, Gateway, MixNode, NodeId, NymNode,
ContractStateParamsUpdate, ExecuteMsg as MixnetExecuteMsg, Gateway, MixNode, NodeId, NymNode,
RoleAssignment,
};
@@ -59,12 +59,27 @@ pub trait MixnetSigningClient {
async fn update_contract_state_params(
&self,
updated_parameters: ContractStateParams,
update: ContractStateParamsUpdate,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_mixnet_contract(
fee,
MixnetExecuteMsg::UpdateContractStateParams { updated_parameters },
MixnetExecuteMsg::UpdateContractStateParams { update },
vec![],
)
.await
}
async fn update_current_nym_node_semver(
&self,
current_nym_node_semver: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_mixnet_contract(
fee,
MixnetExecuteMsg::UpdateCurrentNymNodeSemver {
current_version: current_nym_node_semver,
},
vec![],
)
.await
@@ -682,8 +697,11 @@ mod tests {
MixnetExecuteMsg::UpdateRewardingValidatorAddress { address } => client
.update_rewarding_validator_address(address.parse().unwrap(), None)
.ignore(),
MixnetExecuteMsg::UpdateContractStateParams { updated_parameters } => client
.update_contract_state_params(updated_parameters, None)
MixnetExecuteMsg::UpdateContractStateParams { update } => {
client.update_contract_state_params(update, None).ignore()
}
MixnetExecuteMsg::UpdateCurrentNymNodeSemver { current_version } => client
.update_current_nym_node_semver(current_version, None)
.ignore(),
MixnetExecuteMsg::UpdateActiveSetDistribution {
update,
@@ -303,7 +303,7 @@ where
feature = "tendermint-rpc-http-client",
feature = "tendermint-rpc-websocket-client"
))]
async fn wait_until_healthy<T>(&self, timeout: T) -> Result<(), Error>
async fn wait_until_healthy<T>(&self, timeout: T) -> Result<(), TendermintRpcError>
where
T: Into<core::time::Duration> + Send,
{
@@ -823,7 +823,7 @@ where
feature = "tendermint-rpc-http-client",
feature = "tendermint-rpc-websocket-client"
))]
async fn wait_until_healthy<T>(&self, timeout: T) -> Result<(), Error>
async fn wait_until_healthy<T>(&self, timeout: T) -> Result<(), TendermintRpcError>
where
T: Into<core::time::Duration> + Send,
{
@@ -523,7 +523,7 @@ mod non_wasm {
))]
async fn wait_until_healthy<T>(&self, timeout: T) -> Result<(), Error>
where
T: Into<Duration> + Send,
T: Into<core::time::Duration> + Send,
{
self.wait_until_healthy(timeout).await
}
@@ -98,6 +98,13 @@ impl DirectSecp256k1HdWallet {
Ok((private_key, public_key))
}
pub fn derive_extended_private_key(
&self,
hd_path: &DerivationPath,
) -> Result<XPrv, DirectSecp256k1HdWalletError> {
Ok(XPrv::derive_from_path(self.seed, hd_path)?)
}
pub fn try_derive_accounts(&self) -> Result<Vec<AccountData>, DirectSecp256k1HdWalletError> {
let mut accounts = Vec::with_capacity(self.accounts.len());
for derivation_info in &self.accounts {
@@ -6,6 +6,7 @@ use crate::utils::CommonConfigsWrapper;
use anyhow::{anyhow, bail};
use clap::ArgGroup;
use clap::Parser;
use log::info;
use nym_credential_storage::initialise_persistent_storage;
use nym_credential_storage::storage::Storage;
use nym_credential_utils::utils;
@@ -150,6 +151,7 @@ async fn issue_to_file(args: Args, client: SigningClient) -> anyhow::Result<()>
exported = exported.with_master_verification_key(&EpochVerificationKey { epoch_id, key });
}
info!("the issued ticketbook has expiration of {expiration_date}");
let data = exported.pack().data;
if args.bs58_output {
@@ -33,6 +33,9 @@ pub struct Args {
#[clap(long)]
pub rewarding_denom: Option<String>,
#[clap(long)]
pub current_nym_node_version: String,
#[clap(long, default_value_t = 720)]
pub epochs_in_interval: u32,
@@ -143,6 +146,9 @@ pub async fn generate(args: Args) {
epochs_in_interval: args.epochs_in_interval,
epoch_duration: Duration::from_secs(args.epoch_duration),
initial_rewarding_params,
current_nym_node_version: args.current_nym_node_version,
version_score_weights: Default::default(),
version_score_params: Default::default(),
profit_margin: ProfitMarginRange {
minimum: args.minimum_profit_margin_percent,
maximum: args.maximum_profit_margin_percent,
+12 -1
View File
@@ -1,7 +1,7 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use serde::{Deserialize, Deserializer};
use serde::{Deserialize, Deserializer, Serializer};
use std::fmt::Display;
use std::path::PathBuf;
use std::str::FromStr;
@@ -20,6 +20,17 @@ where
}
}
pub fn ser_maybe_stringified<S, T>(field: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
T: Display,
{
match field {
Some(inner) => serializer.serialize_str(&inner.to_string()),
None => serializer.serialize_str(""),
}
}
pub fn de_maybe_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
@@ -21,3 +21,6 @@ serde_json = { workspace = true }
[build-dependencies]
vergen = { workspace = true, features = ["build", "git", "gitcl", "rustc", "cargo"] }
[features]
naive_float = []
@@ -130,7 +130,7 @@ impl Deref for Percent {
}
// this is not implemented via From traits due to its naive nature and loss of precision
#[cfg(not(target_arch = "wasm32"))]
#[cfg(feature = "naive_float")]
pub trait NaiveFloat {
fn naive_to_f64(&self) -> f64;
@@ -139,8 +139,8 @@ pub trait NaiveFloat {
Self: Sized;
}
#[cfg(not(target_arch = "wasm32"))]
impl NaiveFloat for Percent {
#[cfg(feature = "naive_float")]
impl NaiveFloat for Decimal {
fn naive_to_f64(&self) -> f64 {
use cosmwasm_std::Fraction;
@@ -181,7 +181,21 @@ impl NaiveFloat for Percent {
}
let (n, d) = to_rational(val);
Percent::new(Decimal::from_ratio(n, d))
Ok(Decimal::from_ratio(n, d))
}
}
#[cfg(feature = "naive_float")]
impl NaiveFloat for Percent {
fn naive_to_f64(&self) -> f64 {
self.0.naive_to_f64()
}
fn naive_try_from_f64(val: f64) -> Result<Self, ContractsCommonError>
where
Self: Sized,
{
Percent::new(Decimal::naive_try_from_f64(val)?)
}
}
@@ -47,6 +47,12 @@ impl Deposit {
}
}
#[cw_serde]
#[derive(Default)]
pub struct LatestDepositResponse {
pub deposit: Option<DepositData>,
}
#[cw_serde]
pub struct DepositResponse {
pub id: DepositId,
@@ -1,4 +1,4 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_schema::cw_serde;
@@ -7,7 +7,7 @@ use cosmwasm_std::Coin;
#[cfg(feature = "schema")]
use crate::blacklist::{BlacklistedAccountResponse, PagedBlacklistedAccountResponse};
#[cfg(feature = "schema")]
use crate::deposit::{DepositResponse, PagedDepositsResponse};
use crate::deposit::{DepositResponse, LatestDepositResponse, PagedDepositsResponse};
#[cfg(feature = "schema")]
use cosmwasm_schema::QueryResponses;
@@ -73,6 +73,9 @@ pub enum QueryMsg {
#[cfg_attr(feature = "schema", returns(DepositResponse))]
GetDeposit { deposit_id: u32 },
#[cfg_attr(feature = "schema", returns(LatestDepositResponse))]
GetLatestDeposit {},
#[cfg_attr(feature = "schema", returns(PagedDepositsResponse))]
GetDepositsPaged {
limit: Option<u32>,
@@ -17,6 +17,7 @@ cw-controllers = { workspace = true }
cw2 = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive"] }
serde_repr = { workspace = true }
semver = { workspace = true, features = ["serde"] }
# we still have to preserve that import for `JsonSchema` for `Layer` type (since we can't use cw_serde macro due to custom serde impl)
schemars = { workspace = true }
@@ -26,8 +27,6 @@ serde-json-wasm = { workspace = true }
humantime-serde = { workspace = true }
utoipa = { workspace = true, optional = true }
# TO CHECK WHETHER STILL NEEDED:
log = { workspace = true }
time = { workspace = true, features = ["parsing", "formatting"] }
ts-rs = { workspace = true, optional = true }
@@ -38,6 +37,6 @@ time = { workspace = true, features = ["serde", "macros"] }
[features]
default = []
contract-testing = []
utoipa = [ "dep:utoipa" ]
utoipa = ["dep:utoipa"]
schema = ["cw2"]
generate-ts = ['ts-rs']
@@ -166,6 +166,9 @@ pub enum MixnetContractError {
#[error("Provided message to update rewarding params did not contain any updates")]
EmptyParamsChangeMsg,
#[error("provided message to update state parameters did not contain any updates")]
EmptyStateUpdateMsg,
#[error("one of the roles in the new active set is empty")]
EmptyRoleAssignment,
@@ -269,6 +272,9 @@ pub enum MixnetContractError {
#[error("the total work for this epoch seems to be bigger than 1.0!")]
TotalWorkAboveOne,
#[error("the provided nym-node version is not a valid semver. got: {provided}")]
InvalidNymNodeSemver { provided: String },
}
impl MixnetContractError {
@@ -7,9 +7,9 @@ use crate::mixnode::{MixNodeConfigUpdate, NodeCostParams};
use crate::nym_node::Role;
use crate::reward_params::{ActiveSetUpdate, IntervalRewardParams, IntervalRewardingParamsUpdate};
use crate::rewarding::RewardDistribution;
use crate::{BlockHeight, ContractStateParams, EpochId, IdentityKeyRef, Interval, NodeId};
use crate::{BlockHeight, ContractStateParamsUpdate, EpochId, IdentityKeyRef, Interval, NodeId};
pub use contracts_common::events::*;
use cosmwasm_std::{Addr, Coin, Decimal, Event};
use cosmwasm_std::{attr, Addr, Coin, Decimal, Event};
use std::fmt::Display;
pub const EVENT_VERSION_PREFIX: &str = "v2_";
@@ -45,6 +45,7 @@ pub enum MixnetEventType {
DelegationOnUnbonding,
Undelegation,
ContractSettingsUpdate,
NymNodeSemverUpdate,
RewardingValidatorUpdate,
BeginEpochTransition,
AdvanceEpoch,
@@ -97,6 +98,7 @@ impl Display for MixnetEventType {
MixnetEventType::Delegation => "delegation",
MixnetEventType::Undelegation => "undelegation",
MixnetEventType::ContractSettingsUpdate => "settings_update",
MixnetEventType::NymNodeSemverUpdate => "nym_node_semver_update",
MixnetEventType::RewardingValidatorUpdate => "rewarding_validator_address_update",
MixnetEventType::BeginEpochTransition => "beginning_epoch_transition",
MixnetEventType::AdvanceEpoch => "advance_epoch",
@@ -132,11 +134,13 @@ pub const NODE_ID_KEY: &str = "node_id";
pub const NODE_IDENTITY_KEY: &str = "identity";
// settings change
pub const OLD_MINIMUM_PLEDGE_KEY: &str = "old_minimum_pledge";
pub const OLD_MINIMUM_DELEGATION_KEY: &str = "old_minimum_delegation";
pub const NEW_MINIMUM_PLEDGE_KEY: &str = "new_minimum_pledge";
pub const NEW_MINIMUM_DELEGATION_KEY: &str = "new_minimum_delegation";
pub const NEW_PROFIT_MARGIN_RANGE_KEY: &str = "new_profit_margin_range";
pub const NEW_INTERVAL_OPERATING_COST_RANGE_KEY: &str = "new_interval_operating_cost_range";
pub const NEW_VERSION_WEIGHTS_RANGE_KEY: &str = "new_version_weights_range";
pub const NEW_VERSION_SCORE_FORMULA_PARAMS_KEY: &str = "new_version_score_formula_params";
pub const NYM_NODE_CURRENT_SEMVER_KEY: &str = "new_current_semver";
pub const OLD_REWARDING_VALIDATOR_ADDRESS_KEY: &str = "old_rewarding_validator_address";
pub const NEW_REWARDING_VALIDATOR_ADDRESS_KEY: &str = "new_rewarding_validator_address";
@@ -440,40 +444,73 @@ pub fn new_rewarding_validator_address_update_event(old: Addr, new: Addr) -> Eve
.add_attribute(NEW_REWARDING_VALIDATOR_ADDRESS_KEY, new)
}
pub fn new_settings_update_event(
old_params: &ContractStateParams,
new_params: &ContractStateParams,
) -> Event {
pub fn new_settings_update_event(update: &ContractStateParamsUpdate) -> Event {
let mut event = Event::new(MixnetEventType::ContractSettingsUpdate);
if old_params.minimum_pledge != new_params.minimum_pledge {
event = event
.add_attribute(
OLD_MINIMUM_PLEDGE_KEY,
old_params.minimum_pledge.to_string(),
)
.add_attribute(
NEW_MINIMUM_PLEDGE_KEY,
new_params.minimum_pledge.to_string(),
)
// check for delegations params updates
if let Some(delegations_update) = &update.delegations_params {
event.attributes.push(attr(
NEW_MINIMUM_DELEGATION_KEY,
delegations_update
.minimum_delegation
.as_ref()
.map(|d| d.to_string())
.unwrap_or("empty".to_string()),
));
}
if old_params.minimum_delegation != new_params.minimum_delegation {
if let Some(ref old) = old_params.minimum_delegation {
event = event.add_attribute(OLD_MINIMUM_DELEGATION_KEY, old.to_string())
} else {
event = event.add_attribute(OLD_MINIMUM_DELEGATION_KEY, "None")
// check for operators params updates
if let Some(operators_update) = &update.operators_params {
if let Some(minimum_pledge) = &operators_update.minimum_pledge {
event
.attributes
.push(attr(NEW_MINIMUM_PLEDGE_KEY, minimum_pledge.to_string()))
}
if let Some(ref new) = new_params.minimum_delegation {
event = event.add_attribute(NEW_MINIMUM_DELEGATION_KEY, new.to_string())
} else {
event = event.add_attribute(NEW_MINIMUM_DELEGATION_KEY, "None")
if let Some(profit_margin) = &operators_update.profit_margin {
event
.attributes
.push(attr(NEW_PROFIT_MARGIN_RANGE_KEY, profit_margin.to_string()))
}
if let Some(interval_operating_cost) = &operators_update.interval_operating_cost {
event.attributes.push(attr(
NEW_INTERVAL_OPERATING_COST_RANGE_KEY,
interval_operating_cost.to_string(),
))
}
}
// check for config score params updates
if let Some(config_score_update) = &update.config_score_params {
if let Some(current_nym_node_semver) = &config_score_update.current_nym_node_semver {
event.attributes.push(attr(
NYM_NODE_CURRENT_SEMVER_KEY,
current_nym_node_semver.to_string(),
))
}
if let Some(version_weights) = &config_score_update.version_weights {
event.attributes.push(attr(
NEW_VERSION_WEIGHTS_RANGE_KEY,
format!("{version_weights:?}"),
))
}
if let Some(version_score_formula_params) =
&config_score_update.version_score_formula_params
{
event.attributes.push(attr(
NEW_VERSION_SCORE_FORMULA_PARAMS_KEY,
format!("{version_score_formula_params:?}"),
))
}
}
event
}
pub fn new_update_nym_node_semver_event(new_version: &str) -> Event {
Event::new(MixnetEventType::NymNodeSemverUpdate)
.add_attribute(NYM_NODE_CURRENT_SEMVER_KEY, new_version)
}
pub fn new_not_found_node_operator_rewarding_event(interval: Interval, node_id: NodeId) -> Event {
Event::new(MixnetEventType::NodeRewarding)
.add_attribute(
@@ -12,8 +12,11 @@ use crate::reward_params::{
ActiveSetUpdate, IntervalRewardParams, IntervalRewardingParamsUpdate, NodeRewardingParameters,
Performance, RewardedSetParams, RewardingParams, WorkFactor,
};
use crate::types::{ContractStateParams, NodeId};
use crate::{NymNode, RoleAssignment};
use crate::types::NodeId;
use crate::{
ContractStateParamsUpdate, NymNode, OutdatedVersionWeights, RoleAssignment,
VersionScoreFormulaParams,
};
use crate::{OperatingCostRange, ProfitMarginRange};
use contracts_common::{signing::MessageSignature, IdentityKey, Percent};
use cosmwasm_schema::cw_serde;
@@ -47,7 +50,7 @@ use crate::{
PendingIntervalEventResponse, PendingIntervalEventsResponse,
},
rewarding::{EstimatedCurrentEpochRewardResponse, PendingRewardResponse},
types::ContractState,
types::{ContractState, ContractStateParams},
};
#[cfg(feature = "schema")]
use contracts_common::{signing::Nonce, ContractBuildInformation};
@@ -64,6 +67,14 @@ pub struct InstantiateMsg {
pub epoch_duration: Duration,
pub initial_rewarding_params: InitialRewardingParams,
pub current_nym_node_version: String,
#[serde(default)]
pub version_score_weights: OutdatedVersionWeights,
#[serde(default)]
pub version_score_params: VersionScoreFormulaParams,
#[serde(default)]
pub profit_margin: ProfitMarginRange,
@@ -126,7 +137,10 @@ pub enum ExecuteMsg {
address: String,
},
UpdateContractStateParams {
updated_parameters: ContractStateParams,
update: ContractStateParamsUpdate,
},
UpdateCurrentNymNodeSemver {
current_version: String,
},
UpdateActiveSetDistribution {
update: ActiveSetUpdate,
@@ -295,6 +309,9 @@ impl ExecuteMsg {
ExecuteMsg::UpdateContractStateParams { .. } => {
"updating mixnet state parameters".into()
}
ExecuteMsg::UpdateCurrentNymNodeSemver { current_version } => {
format!("updating current nym-node semver to {current_version}")
}
ExecuteMsg::UpdateActiveSetDistribution {
force_immediately, ..
} => format!("updating active set distribution. forced: {force_immediately}"),
@@ -829,6 +846,13 @@ pub enum QueryMsg {
#[cw_serde]
pub struct MigrateMsg {
pub vesting_contract_address: Option<String>,
pub unsafe_skip_state_updates: Option<bool>,
pub vesting_contract_address: Option<String>,
pub current_nym_node_semver: String,
#[serde(default)]
pub version_score_weights: OutdatedVersionWeights,
#[serde(default)]
pub version_score_params: VersionScoreFormulaParams,
}
@@ -4,8 +4,8 @@
use crate::nym_node::Role;
use contracts_common::Percent;
use cosmwasm_schema::cw_serde;
use cosmwasm_std::Coin;
use cosmwasm_std::{Addr, Uint128};
use cosmwasm_std::{Coin, Decimal};
use std::fmt::{Display, Formatter};
// type aliases for better reasoning about available data
@@ -161,19 +161,156 @@ pub struct ContractState {
/// Contract parameters that could be adjusted in a transaction by the contract admin.
#[cw_serde]
pub struct ContractStateParams {
/// Parameters to do with delegations.
pub delegations_params: DelegationsParams,
/// Parameters to do with node operators.
pub operators_params: OperatorsParams,
/// Parameters to do with the config score
pub config_score_params: ConfigScoreParams,
}
#[cw_serde]
pub struct ContractStateParamsUpdate {
pub delegations_params: Option<DelegationsParams>,
pub operators_params: Option<OperatorsParamsUpdate>,
pub config_score_params: Option<ConfigScoreParamsUpdate>,
}
impl ContractStateParamsUpdate {
pub fn contains_updates(&self) -> bool {
self.delegations_params.is_some()
|| self.operators_params.is_some()
|| self.config_score_params.is_some()
}
}
#[cw_serde]
pub struct DelegationsParams {
/// Minimum amount a delegator must stake in orders for his delegation to get accepted.
pub minimum_delegation: Option<Coin>,
}
#[cw_serde]
pub struct OperatorsParams {
/// Minimum amount a node must pledge to get into the system.
pub minimum_pledge: Coin,
/// Defines the allowed profit margin range of operators.
/// default: 0% - 100%
#[serde(default)]
pub profit_margin: ProfitMarginRange,
/// Defines the allowed interval operating cost range of operators.
/// default: 0 - 1'000'000'000'000'000 (1 Billion native tokens - the total supply)
#[serde(default)]
pub interval_operating_cost: OperatingCostRange,
}
#[cw_serde]
pub struct OperatorsParamsUpdate {
pub minimum_pledge: Option<Coin>,
pub profit_margin: Option<ProfitMarginRange>,
pub interval_operating_cost: Option<OperatingCostRange>,
}
impl OperatorsParamsUpdate {
pub fn contains_updates(&self) -> bool {
self.minimum_pledge.is_some()
|| self.profit_margin.is_some()
|| self.interval_operating_cost.is_some()
}
}
#[cw_serde]
pub struct ConfigScoreParams {
/// Current version of the nym node that is going to be used for determining the version score of a node.
/// note: value stored here is pre-validated `semver::Version`
pub current_nym_node_semver: String,
/// Defines weights for calculating numbers of versions behind the current release.
pub version_weights: OutdatedVersionWeights,
/// Defines the parameters of the formula for calculating the version score
pub version_score_formula_params: VersionScoreFormulaParams,
}
impl ConfigScoreParams {
// SAFETY: the value stored in the contract is always valid
#[allow(clippy::unwrap_used)]
pub fn unchecked_nym_node_version(&self) -> semver::Version {
self.current_nym_node_semver.parse().unwrap()
}
pub fn versions_behind(&self, node_semver: &semver::Version) -> u32 {
let expected = self.unchecked_nym_node_version();
let major_diff = (node_semver.major as i64 - expected.major as i64).unsigned_abs() as u32;
let minor_diff = (node_semver.minor as i64 - expected.minor as i64).unsigned_abs() as u32;
let patch_diff = (node_semver.patch as i64 - expected.patch as i64).unsigned_abs() as u32;
let prerelease_diff = if node_semver.pre == expected.pre {
0
} else {
1
};
major_diff * self.version_weights.major
+ minor_diff * self.version_weights.minor
+ patch_diff * self.version_weights.patch
+ prerelease_diff * self.version_weights.prerelease
}
}
/// Defines weights for calculating numbers of versions behind the current release.
#[cw_serde]
#[derive(Copy)]
pub struct OutdatedVersionWeights {
pub major: u32,
pub minor: u32,
pub patch: u32,
pub prerelease: u32,
}
impl Default for OutdatedVersionWeights {
fn default() -> Self {
OutdatedVersionWeights {
major: 100,
minor: 10,
patch: 1,
prerelease: 1,
}
}
}
/// Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling)
/// define the relevant parameters
#[cw_serde]
#[derive(Copy)]
pub struct VersionScoreFormulaParams {
pub penalty: Decimal,
pub penalty_scaling: Decimal,
}
impl Default for VersionScoreFormulaParams {
fn default() -> Self {
#[allow(clippy::unwrap_used)]
VersionScoreFormulaParams {
penalty: "0.8".parse().unwrap(),
penalty_scaling: "2.0".parse().unwrap(),
}
}
}
#[cw_serde]
pub struct ConfigScoreParamsUpdate {
pub current_nym_node_semver: Option<String>,
pub version_weights: Option<OutdatedVersionWeights>,
pub version_score_formula_params: Option<VersionScoreFormulaParams>,
}
impl ConfigScoreParamsUpdate {
pub fn contains_updates(&self) -> bool {
self.current_nym_node_semver.is_some()
|| self.version_weights.is_some()
|| self.version_score_formula_params.is_some()
}
}
@@ -15,7 +15,9 @@ mixnet-contract-common = { path = "../mixnet-contract", package = "nym-mixnet-co
contracts-common = { path = "../contracts-common", package = "nym-contracts-common", version = "0.5.0" }
serde = { workspace = true, features = ["derive"] }
thiserror = { workspace = true }
ts-rs = { workspace = true, optional = true}
# without this feature, cargo clippy emits a ton of incompatibility warnings
# https://docs.rs/ts-rs/latest/ts_rs/#serde-compatability
ts-rs = { workspace = true, optional = true, features = ["no-serde-warnings"] }
[features]
schema = ["cw2"]
+9 -1
View File
@@ -221,7 +221,15 @@ impl From<PayInfo> for NymPayInfo {
}
#[derive(
Copy, Clone, Debug, PartialEq, Serialize, Deserialize, strum::Display, strum::EnumString,
Copy,
Clone,
Debug,
PartialEq,
Serialize,
Deserialize,
strum::Display,
strum::EnumString,
strum::EnumIter,
)]
#[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "kebab-case")]
+127
View File
@@ -282,6 +282,48 @@ impl Client {
}
}
pub async fn create_delete_request<K, V>(
&self,
path: PathSegments<'_>,
params: Params<'_, K, V>,
) -> RequestBuilder
where
K: AsRef<str>,
V: AsRef<str>,
{
let url = sanitize_url(&self.base_url, path, params);
self.reqwest_client.delete(url)
}
pub async fn send_delete_request<K, V, E>(
&self,
path: PathSegments<'_>,
params: Params<'_, K, V>,
) -> Result<Response, HttpClientError<E>>
where
K: AsRef<str>,
V: AsRef<str>,
E: Display,
{
tracing::trace!("Sending DELETE request");
let url = sanitize_url(&self.base_url, path, params);
#[cfg(target_arch = "wasm32")]
{
Ok(wasmtimer::tokio::timeout(
self.request_timeout,
self.reqwest_client.delete(url).send(),
)
.await
.map_err(|_timeout| HttpClientError::RequestTimeout)??)
}
#[cfg(not(target_arch = "wasm32"))]
{
Ok(self.reqwest_client.delete(url).send().await?)
}
}
#[instrument(level = "debug", skip_all)]
pub async fn get_json<T, K, V, E>(
&self,
@@ -315,6 +357,22 @@ impl Client {
parse_response(res, true).await
}
pub async fn delete_json<T, K, V, E>(
&self,
path: PathSegments<'_>,
params: Params<'_, K, V>,
) -> Result<T, HttpClientError<E>>
where
for<'a> T: Deserialize<'a>,
K: AsRef<str>,
V: AsRef<str>,
E: Display + DeserializeOwned,
{
let res = self.send_delete_request(path, params).await?;
parse_response(res, false).await
}
#[instrument(level = "debug", skip_all)]
pub async fn get_json_endpoint<T, S, E>(&self, endpoint: S) -> Result<T, HttpClientError<E>>
where
for<'a> T: Deserialize<'a>,
@@ -379,6 +437,35 @@ impl Client {
parse_response(res, true).await
}
pub async fn delete_json_endpoint<T, S, E>(&self, endpoint: S) -> Result<T, HttpClientError<E>>
where
for<'a> T: Deserialize<'a>,
E: Display + DeserializeOwned,
S: AsRef<str>,
{
#[cfg(target_arch = "wasm32")]
let res = {
wasmtimer::tokio::timeout(
self.request_timeout,
self.reqwest_client
.delete(self.base_url.join(endpoint.as_ref())?)
.send(),
)
.await
.map_err(|_timeout| HttpClientError::RequestTimeout)??
};
#[cfg(not(target_arch = "wasm32"))]
let res = {
self.reqwest_client
.delete(self.base_url.join(endpoint.as_ref())?)
.send()
.await?
};
parse_response(res, false).await
}
}
// define those methods on the trait for nicer extensions (and not having to type the thing twice)
@@ -411,6 +498,17 @@ pub trait ApiClient {
V: AsRef<str> + Sync,
E: Display + DeserializeOwned;
async fn delete_json<T, K, V, E>(
&self,
path: PathSegments<'_>,
params: Params<'_, K, V>,
) -> Result<T, HttpClientError<E>>
where
for<'a> T: Deserialize<'a>,
K: AsRef<str> + Sync,
V: AsRef<str> + Sync,
E: Display + DeserializeOwned;
/// `get` json data from the provided absolute endpoint, i.e. for example `"/api/v1/mixnodes?since=12345"`
async fn get_json_from<T, S, E>(&self, endpoint: S) -> Result<T, HttpClientError<E>>
where
@@ -428,6 +526,12 @@ pub trait ApiClient {
for<'a> T: Deserialize<'a>,
E: Display + DeserializeOwned,
S: AsRef<str> + Sync + Send;
async fn delete_json_from<T, S, E>(&self, endpoint: S) -> Result<T, HttpClientError<E>>
where
for<'a> T: Deserialize<'a>,
E: Display + DeserializeOwned,
S: AsRef<str> + Sync + Send;
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -463,6 +567,20 @@ impl ApiClient for Client {
self.post_json(path, params, json_body).await
}
async fn delete_json<T, K, V, E>(
&self,
path: PathSegments<'_>,
params: Params<'_, K, V>,
) -> Result<T, HttpClientError<E>>
where
for<'a> T: Deserialize<'a>,
K: AsRef<str> + Sync,
V: AsRef<str> + Sync,
E: Display + DeserializeOwned,
{
self.delete_json(path, params).await
}
async fn get_json_from<T, S, E>(&self, endpoint: S) -> Result<T, HttpClientError<E>>
where
for<'a> T: Deserialize<'a>,
@@ -485,6 +603,15 @@ impl ApiClient for Client {
{
self.post_json_endpoint(endpoint, json_body).await
}
async fn delete_json_from<T, S, E>(&self, endpoint: S) -> Result<T, HttpClientError<E>>
where
for<'a> T: Deserialize<'a>,
E: Display + DeserializeOwned,
S: AsRef<str> + Sync + Send,
{
self.delete_json_endpoint(endpoint).await
}
}
// utility function that should solve the double slash problem in API urls forever.
+3
View File
@@ -11,4 +11,7 @@ rust-version.workspace = true
readme.workspace = true
[dependencies]
anyhow = { workspace = true }
bincode = { workspace = true }
nym-crypto = { path = "../crypto", features = ["asymmetric", "serde"] }
serde = { workspace = true, features = ["derive"] }
+94
View File
@@ -1,7 +1,101 @@
use nym_crypto::asymmetric::ed25519::{PublicKey, Signature, SignatureError};
use serde::{Deserialize, Serialize};
pub mod get_testrun {
use super::*;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Payload {
pub agent_public_key: PublicKey,
pub timestamp: i64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GetTestrunRequest {
pub payload: Payload,
pub signature: Signature,
}
impl SignedRequest for GetTestrunRequest {
type Payload = Payload;
fn public_key(&self) -> &PublicKey {
&self.payload.agent_public_key
}
fn signature(&self) -> &Signature {
&self.signature
}
fn payload(&self) -> &Self::Payload {
&self.payload
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TestrunAssignment {
pub testrun_id: i64,
pub assigned_at_utc: i64,
pub gateway_identity_key: String,
}
pub mod submit_results {
use super::*;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Payload {
pub probe_result: String,
pub agent_public_key: PublicKey,
pub assigned_at_utc: i64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SubmitResults {
pub payload: Payload,
pub signature: Signature,
}
impl SignedRequest for SubmitResults {
type Payload = Payload;
fn public_key(&self) -> &PublicKey {
&self.payload.agent_public_key
}
fn signature(&self) -> &Signature {
&self.signature
}
fn payload(&self) -> &Self::Payload {
&self.payload
}
}
}
pub trait SignedRequest {
type Payload: serde::Serialize;
fn public_key(&self) -> &PublicKey;
fn signature(&self) -> &Signature;
fn payload(&self) -> &Self::Payload;
}
pub trait VerifiableRequest: SignedRequest {
type Error: From<bincode::Error> + From<SignatureError>;
fn verify_signature(&self) -> Result<(), Self::Error> {
bincode::serialize(self.payload())
.map_err(Self::Error::from)
.and_then(|serialized| {
self.public_key()
.verify(serialized, self.signature())
.map_err(Self::Error::from)
})
}
}
impl<T> VerifiableRequest for T
where
T: SignedRequest,
{
type Error = anyhow::Error;
}
+40
View File
@@ -157,6 +157,46 @@ impl NymNetworkDetails {
}
}
#[rustfmt::skip]
#[cfg(feature = "env")]
pub fn export_to_env(self) {
use crate::var_names;
use std::env::set_var;
fn set_optional_var(var_name: &str, value: Option<String>) {
if let Some(value) = value {
set_var(var_name, value);
}
}
set_var(var_names::NETWORK_NAME, self.network_name);
set_var(var_names::BECH32_PREFIX, self.chain_details.bech32_account_prefix);
set_var(var_names::MIX_DENOM, self.chain_details.mix_denom.base);
set_var(var_names::MIX_DENOM_DISPLAY, self.chain_details.mix_denom.display);
set_var(var_names::STAKE_DENOM, self.chain_details.stake_denom.base);
set_var(var_names::STAKE_DENOM_DISPLAY, self.chain_details.stake_denom.display);
set_var(var_names::DENOMS_EXPONENT, self.chain_details.mix_denom.display_exponent.to_string());
if let Some(e) = self.endpoints.first() {
set_var(var_names::NYXD, e.nyxd_url.clone());
set_optional_var(var_names::NYM_API, e.api_url.clone());
set_optional_var(var_names::NYXD_WEBSOCKET, e.websocket_url.clone());
}
set_optional_var(var_names::MIXNET_CONTRACT_ADDRESS, self.contracts.mixnet_contract_address);
set_optional_var(var_names::VESTING_CONTRACT_ADDRESS, self.contracts.vesting_contract_address);
set_optional_var(var_names::ECASH_CONTRACT_ADDRESS, self.contracts.ecash_contract_address);
set_optional_var(var_names::GROUP_CONTRACT_ADDRESS, self.contracts.group_contract_address);
set_optional_var(var_names::MULTISIG_CONTRACT_ADDRESS, self.contracts.multisig_contract_address);
set_optional_var(var_names::COCONUT_DKG_CONTRACT_ADDRESS, self.contracts.coconut_dkg_contract_address);
set_optional_var(var_names::EXPLORER_API, self.explorer_api);
set_optional_var(var_names::NYM_VPN_API, self.nym_vpn_api_url);
}
pub fn default_gas_price_amount(&self) -> f64 {
GAS_PRICE_AMOUNT
}
+45
View File
@@ -0,0 +1,45 @@
[package]
name = "nym-tui-common"
version = "0.1.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
readme.workspace = true
[dependencies]
async-trait = { workspace = true }
humantime-serde = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true, features = ["sync", "rt-multi-thread", "signal", "macros"] }
tokio-stream = { workspace = true }
tokio-util = { workspace = true, features = ["rt"] }
tracing = { workspace = true }
thiserror = { workspace = true }
# tui
crossterm = { workspace = true, features = ["event-stream"] }
ratatui = { workspace = true, features = ["serde", "macros", "crossterm"] }
tui-logger = { workspace = true, optional = true, features = ["tracing-support"], default-features = false }
# panic handlers
better-panic = { workspace = true }
color-eyre = { workspace = true }
strip-ansi-escapes = { workspace = true }
[dev-dependencies]
tracing-subscriber = { workspace = true, features = ["env-filter"] }
tracing = { workspace = true, features = ["log"] }
[[example]]
name = "hello_world"
required-features = ["logger"]
[features]
logger = ["tui-logger"]
@@ -0,0 +1,175 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use async_trait::async_trait;
use color_eyre::eyre;
use crossterm::event::{KeyCode, KeyEvent};
use nym_tui_common::tui::config::keybindings::{KeyBinding, LoggerKeybindings};
use nym_tui_common::{
run_tui, Action, ActionDispatcher, ActionSender, AppAction, Component, DebugHistory, Logger,
LoggerProps, State,
};
use ratatui::layout::{Layout, Rect};
use ratatui::prelude::Constraint;
use ratatui::widgets::Paragraph;
use ratatui::Frame;
use tracing::log::LevelFilter;
use tracing_subscriber::EnvFilter;
// useful pattern for tabs, etc:
/*
fn get_active_page_component_mut(&mut self) -> &mut dyn Component {
match self.props.active_tab {
ActiveTab::Tab1 => &mut self.tab1,
ActiveTab::Tab2 => &mut self.tab2,
}
}
*/
struct Props {
custom_quit: KeyBinding,
}
impl From<&HelloStore> for Props {
fn from(store: &HelloStore) -> Self {
Props {
custom_quit: store.config.custom_quit,
}
}
}
pub struct HelloRootApp {
props: Props,
action_sender: ActionSender<HelloActions>,
logger: Logger<HelloStore, HelloActions>,
debug_history: DebugHistory<HelloStore, HelloActions>,
}
impl Component for HelloRootApp {
type State = HelloStore;
type Actions = HelloActions;
fn new(state: &HelloStore, action_sender: ActionSender<HelloActions>) -> Self
where
Self: Sized,
{
HelloRootApp {
props: Props::from(state),
action_sender: action_sender.clone(),
logger: Logger::new(state, action_sender.clone()),
debug_history: DebugHistory::new(state, action_sender),
}
}
fn update(self, state: &Self::State) -> Self
where
Self: Sized,
{
HelloRootApp {
logger: self.logger.update(state),
debug_history: self.debug_history.update(state),
..self
}
}
fn tick(&mut self) -> bool {
let logger_tick = self.logger.tick();
let debug_history_tick = self.debug_history.tick();
logger_tick || debug_history_tick
}
fn handle_key(&mut self, key: KeyEvent) -> eyre::Result<()> {
let maybe_binding = KeyBinding::from(key);
if maybe_binding == self.props.custom_quit {
self.action_sender.send(Action::Quit);
}
self.logger.handle_key(key)?;
self.debug_history.handle_key(key)?;
Ok(())
}
fn view(&mut self, frame: &mut Frame, rect: Rect) {
let [logs, hello_rect] =
Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]).areas(rect);
self.logger.view(frame, logs);
frame.render_widget(Paragraph::new("Hello world!").centered(), hello_rect);
self.debug_history.view(frame, rect);
}
}
#[derive(Debug, Clone)]
pub enum HelloActions {}
impl AppAction for HelloActions {}
#[derive(Clone)]
pub struct HelloConfig {
pub custom_quit: KeyBinding,
pub logger_keybindings: LoggerKeybindings,
}
impl Default for HelloConfig {
fn default() -> Self {
HelloConfig {
custom_quit: KeyBinding::new(KeyCode::Char('x')),
logger_keybindings: Default::default(),
}
}
}
#[derive(Clone, Default)]
pub struct HelloStore {
config: HelloConfig,
}
impl<'a> From<&'a HelloStore> for LoggerProps {
fn from(store: &'a HelloStore) -> LoggerProps {
LoggerProps {
keybindings: store.config.logger_keybindings,
}
}
}
impl State for HelloStore {}
#[derive(Default)]
pub struct HelloDispatcher {}
#[async_trait]
impl ActionDispatcher for HelloDispatcher {
type Store = HelloStore;
type Actions = HelloActions;
async fn handle_app_action(
&mut self,
action: Self::Actions,
store: &mut Self::Store,
) -> eyre::Result<()> {
let _ = action;
let _ = store;
Ok(())
}
}
#[tokio::main]
async fn main() -> eyre::Result<()> {
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::Layer;
let filter: EnvFilter = "trace,mio=warn".parse()?;
tracing_subscriber::registry()
.with(tui_logger::tracing_subscriber_layer().with_filter(filter))
.init();
tui_logger::init_logger(LevelFilter::Trace)?;
run_tui::<HelloRootApp, _>(Default::default(), HelloDispatcher {}, Default::default()).await
}
+24
View File
@@ -0,0 +1,24 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::char::ParseCharError;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum NymTuiError {
#[error("failed to abort tui processing task within specified duration")]
TuiTaskAbortFailure,
#[error("{str} could not be parsed into a character: {source}")]
InvalidCharacter {
str: String,
#[source]
source: ParseCharError,
},
#[error("could not process an unknown keybinding: '{value}'")]
UnknownKeyBinding { value: String },
#[error("could not process an unknown key modifier: '{value}'")]
UnknownKeyModifier { value: String },
}
+46
View File
@@ -0,0 +1,46 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![warn(clippy::expect_used)]
#![warn(clippy::unwrap_used)]
#![warn(clippy::todo)]
#![warn(clippy::dbg_macro)]
#![warn(clippy::panic)]
use crate::tui::manager::TuiManager;
use color_eyre::eyre;
pub mod error;
pub mod tui;
pub use crate::tui::config::TuiConfig;
pub use tui::action::{Action, AppAction};
pub use tui::dispatcher::store::State;
pub use tui::dispatcher::{ActionDispatcher, ActionSender};
pub use tui::initialize_panic_handler;
pub use tui::ui::components::Component;
// components:
pub use tui::ui::components::common::DebugHistory;
#[cfg(feature = "logger")]
pub use tui::ui::components::common::{Logger, LoggerProps};
pub async fn run_tui<C, D>(
config: TuiConfig,
action_dispatcher: D,
initial_state: D::Store,
) -> eyre::Result<()>
where
C: Component + Send + Sync + 'static,
C::State: Send + Sync + 'static,
C::Actions: Send + Sync + Clone + 'static,
D: ActionDispatcher<Store = C::State, Actions = C::Actions> + Send + Sync + 'static,
{
initialize_panic_handler()?;
TuiManager::<C>::build_new(config, action_dispatcher, initial_state)?
.wait_for_exit_or_signal()
.await;
Ok(())
}
+13
View File
@@ -0,0 +1,13 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::fmt::Debug;
#[derive(Debug)]
pub enum Action<T: AppAction> {
Quit,
AppDefined(T),
}
pub trait AppAction: Debug {}
@@ -0,0 +1,271 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use std::sync::LazyLock;
use crate::error::NymTuiError;
#[cfg(feature = "logger")]
use tui_logger::TuiWidgetEvent;
static KEY_MODIFIERS: LazyLock<HashMap<&'static str, KeyModifiers>> = LazyLock::new(|| {
let mut m = HashMap::new();
m.insert("shift", KeyModifiers::SHIFT);
m.insert("ctrl", KeyModifiers::CONTROL);
m.insert("alt", KeyModifiers::ALT);
m.insert("super", KeyModifiers::SUPER);
m.insert("hyper", KeyModifiers::HYPER);
m.insert("meta", KeyModifiers::META);
m
});
static SPECIAL_KEYS: LazyLock<HashMap<&'static str, KeyCode>> = LazyLock::new(|| {
let mut m = HashMap::new();
m.insert("backspace", KeyCode::Backspace);
m.insert("enter", KeyCode::Enter);
m.insert("left", KeyCode::Left);
m.insert("right", KeyCode::Right);
m.insert("up", KeyCode::Up);
m.insert("down", KeyCode::Down);
m.insert("home", KeyCode::Home);
m.insert("end", KeyCode::End);
m.insert("pageup", KeyCode::PageUp);
m.insert("pagedown", KeyCode::PageDown);
m.insert("tab", KeyCode::Tab);
m.insert("backtab", KeyCode::BackTab);
m.insert("delete", KeyCode::Delete);
m.insert("insert", KeyCode::Insert);
m.insert("null", KeyCode::Null);
m.insert("esc", KeyCode::Esc);
m.insert("space", KeyCode::Char(' '));
m.insert("f1", KeyCode::F(1));
m.insert("f2", KeyCode::F(2));
m.insert("f3", KeyCode::F(3));
m.insert("f4", KeyCode::F(4));
m.insert("f5", KeyCode::F(5));
m.insert("f6", KeyCode::F(6));
m.insert("f7", KeyCode::F(7));
m.insert("f8", KeyCode::F(8));
m.insert("f9", KeyCode::F(9));
m.insert("f10", KeyCode::F(10));
m.insert("f11", KeyCode::F(11));
m.insert("f12", KeyCode::F(12));
m
});
#[cfg(feature = "logger")]
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
pub struct LoggerKeybindings {
// TODO: give them better names
tui_logger_space_key: KeyBinding,
tui_logger_up_key: KeyBinding,
tui_logger_down_key: KeyBinding,
tui_logger_left_key: KeyBinding,
tui_logger_right_key: KeyBinding,
tui_logger_plus_key: KeyBinding,
tui_logger_minus_key: KeyBinding,
tui_logger_hide_key: KeyBinding,
tui_logger_focus_key: KeyBinding,
tui_logger_prev_page_key: KeyBinding,
tui_logger_next_page_key: KeyBinding,
tui_logger_escape_key: KeyBinding,
}
#[cfg(feature = "logger")]
impl Default for LoggerKeybindings {
fn default() -> Self {
LoggerKeybindings {
tui_logger_space_key: KeyBinding::new(KeyCode::Char(' ')),
tui_logger_up_key: KeyBinding::new(KeyCode::Up),
tui_logger_down_key: KeyBinding::new(KeyCode::Down),
tui_logger_left_key: KeyBinding::new(KeyCode::Left),
tui_logger_right_key: KeyBinding::new(KeyCode::Right),
tui_logger_plus_key: KeyBinding::new(KeyCode::Char('+')),
tui_logger_minus_key: KeyBinding::new(KeyCode::Char('-')),
tui_logger_hide_key: KeyBinding::new(KeyCode::Char('h')),
tui_logger_focus_key: KeyBinding::new(KeyCode::Char('f')),
tui_logger_prev_page_key: KeyBinding::new(KeyCode::PageUp),
tui_logger_next_page_key: KeyBinding::new(KeyCode::PageDown),
tui_logger_escape_key: KeyBinding::new(KeyCode::Esc),
}
}
}
#[cfg(feature = "logger")]
impl LoggerKeybindings {
pub fn tui_logger_event(&self, key: KeyBinding) -> Option<TuiWidgetEvent> {
if key == self.tui_logger_space_key {
Some(TuiWidgetEvent::SpaceKey)
} else if key == self.tui_logger_up_key {
Some(TuiWidgetEvent::UpKey)
} else if key == self.tui_logger_down_key {
Some(TuiWidgetEvent::DownKey)
} else if key == self.tui_logger_left_key {
Some(TuiWidgetEvent::LeftKey)
} else if key == self.tui_logger_right_key {
Some(TuiWidgetEvent::RightKey)
} else if key == self.tui_logger_plus_key {
Some(TuiWidgetEvent::PlusKey)
} else if key == self.tui_logger_minus_key {
Some(TuiWidgetEvent::MinusKey)
} else if key == self.tui_logger_hide_key {
Some(TuiWidgetEvent::HideKey)
} else if key == self.tui_logger_focus_key {
Some(TuiWidgetEvent::FocusKey)
} else if key == self.tui_logger_prev_page_key {
Some(TuiWidgetEvent::PrevPageKey)
} else if key == self.tui_logger_next_page_key {
Some(TuiWidgetEvent::NextPageKey)
} else if key == self.tui_logger_escape_key {
Some(TuiWidgetEvent::EscapeKey)
} else {
None
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, PartialEq, Hash)]
#[serde(try_from = "String")]
#[serde(into = "String")]
pub struct KeyBinding {
pub code: KeyCode,
pub modifier: KeyModifiers,
}
impl KeyBinding {
pub fn new(code: KeyCode) -> Self {
KeyBinding {
code,
modifier: KeyModifiers::NONE,
}
}
pub fn new_with_modifier(code: KeyCode, modifier: KeyModifiers) -> Self {
KeyBinding { code, modifier }
}
}
impl From<KeyEvent> for KeyBinding {
fn from(value: KeyEvent) -> Self {
KeyBinding {
code: value.code,
modifier: value.modifiers,
}
}
}
impl Display for KeyBinding {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if self.modifier.contains(KeyModifiers::SHIFT) {
write!(f, "shift+")?;
}
if self.modifier.contains(KeyModifiers::CONTROL) {
write!(f, "ctrl+")?;
}
if self.modifier.contains(KeyModifiers::ALT) {
write!(f, "alt+")?;
}
if self.modifier.contains(KeyModifiers::SUPER) {
write!(f, "super+")?;
}
if self.modifier.contains(KeyModifiers::HYPER) {
write!(f, "hyper+")?;
}
if self.modifier.contains(KeyModifiers::META) {
write!(f, "meta+")?;
}
match self.code {
KeyCode::Backspace => write!(f, "backspace"),
KeyCode::Enter => write!(f, "enter"),
KeyCode::Left => write!(f, "left"),
KeyCode::Right => write!(f, "right"),
KeyCode::Up => write!(f, "up"),
KeyCode::Down => write!(f, "down"),
KeyCode::Home => write!(f, "home"),
KeyCode::End => write!(f, "end"),
KeyCode::PageUp => write!(f, "pageup"),
KeyCode::PageDown => write!(f, "pagedown"),
KeyCode::Tab => write!(f, "tab"),
KeyCode::BackTab => write!(f, "backtab"),
KeyCode::Delete => write!(f, "delete"),
KeyCode::Insert => write!(f, "insert"),
KeyCode::Char(c) => write!(f, "{c}"),
KeyCode::Null => write!(f, "null"),
KeyCode::Esc => write!(f, "esc"),
_ => write!(f, "unknown"),
}
}
}
impl From<KeyBinding> for String {
fn from(value: KeyBinding) -> Self {
value.to_string()
}
}
impl TryFrom<String> for KeyBinding {
type Error = NymTuiError;
fn try_from(value: String) -> Result<Self, Self::Error> {
value.parse()
}
}
impl FromStr for KeyBinding {
type Err = NymTuiError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.rsplit_once('+') {
Some((modifiers, value)) => Ok(Self {
code: parse_keycode(value)?,
modifier: parse_modifiers(modifiers)?,
}),
None => Ok(Self {
code: parse_keycode(s)?,
modifier: KeyModifiers::NONE,
}),
}
}
}
fn parse_keycode(value: &str) -> Result<KeyCode, NymTuiError> {
Ok(if value.len() == 1 {
KeyCode::Char(
char::from_str(value)
.map_err(|source| NymTuiError::InvalidCharacter {
str: value.to_string(),
source,
})?
.to_ascii_lowercase(),
)
} else {
SPECIAL_KEYS
.get(value)
.cloned()
.ok_or_else(|| NymTuiError::UnknownKeyBinding {
value: value.to_string(),
})?
})
}
fn parse_modifiers(modifiers: &str) -> Result<KeyModifiers, NymTuiError> {
modifiers
.split('+')
.try_fold(KeyModifiers::NONE, |modifiers, token| {
KEY_MODIFIERS
.get(token)
.map(|modifier| modifiers | *modifier)
.ok_or_else(|| NymTuiError::UnknownKeyModifier {
value: token.to_string(),
})
})
}
pub fn key_event_to_string(key_event: &KeyEvent) -> String {
KeyBinding::from(*key_event).to_string()
}
@@ -0,0 +1,69 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use serde::{Deserialize, Serialize};
use std::time::Duration;
pub mod keybindings;
pub const DEFAULT_TICK_RATE: Duration = Duration::from_millis(200);
const DEFAULT_SHUTDOWN_GRACE: Duration = Duration::from_millis(500);
const DEFAULT_CANCEL_GRACE: Duration = Duration::from_millis(500);
const DEFAULT_ABORT_GRACE: Duration = Duration::from_millis(200);
#[derive(Default, Debug, Serialize, Deserialize, Clone)]
pub struct TuiConfig {
pub tui: Tui,
// #[serde(default)]
// pub key_bindings: KeyBindings,
#[serde(default)]
pub debug: Debug,
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
pub struct Debug {
pub debug_mode_enabled: bool,
#[serde(with = "humantime_serde")]
pub shutdown_grace: Duration,
#[serde(with = "humantime_serde")]
pub cancel_grace: Duration,
#[serde(with = "humantime_serde")]
pub abort_grace: Duration,
}
impl Default for Debug {
fn default() -> Self {
Debug {
debug_mode_enabled: true,
shutdown_grace: DEFAULT_SHUTDOWN_GRACE,
cancel_grace: DEFAULT_CANCEL_GRACE,
abort_grace: DEFAULT_ABORT_GRACE,
}
}
}
#[derive(Default, Debug, Serialize, Deserialize, Clone, Copy)]
pub struct Tui {
#[serde(default, flatten)]
pub debug: TuiDebug,
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
pub struct TuiDebug {
#[serde(with = "humantime_serde")]
pub tick_rate: Duration,
}
impl Default for TuiDebug {
fn default() -> Self {
TuiDebug {
tick_rate: DEFAULT_TICK_RATE,
// frame_rate: DEFAULT_FRAME_RATE,
}
}
}
@@ -0,0 +1,139 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::tui::action::Action;
use crate::tui::dispatcher::store::State;
use crate::AppAction;
use async_trait::async_trait;
use color_eyre::eyre;
use tokio::sync::mpsc::UnboundedSender;
use tokio_stream::wrappers::UnboundedReceiverStream;
use tokio_stream::StreamExt;
use tokio_util::sync::CancellationToken;
use tracing::{debug, error, info, warn};
pub mod store;
#[derive(Clone)]
pub struct ActionSender<T: AppAction>(UnboundedSender<Action<T>>);
impl<T> From<UnboundedSender<Action<T>>> for ActionSender<T>
where
T: AppAction,
{
fn from(value: UnboundedSender<Action<T>>) -> Self {
ActionSender(value)
}
}
impl<T> ActionSender<T>
where
T: AppAction,
{
pub fn send(&self, action: impl Into<Action<T>>) {
if let Err(unsent) = self.0.send(action.into()) {
error!("failed to send {:?} action to the dispatcher", unsent.0)
}
}
}
pub type ActionReceiver<T> = UnboundedReceiverStream<Action<T>>;
pub type StateUpdateSender<S> = UnboundedSender<S>;
pub type StateUpdateReceiver<S> = UnboundedReceiverStream<S>;
#[async_trait]
pub trait ActionDispatcher {
type Store: State;
type Actions: AppAction;
async fn handle_app_action(
&mut self,
action: Self::Actions,
store: &mut Self::Store,
) -> eyre::Result<()>;
}
pub struct DispatcherLoop<D: ActionDispatcher> {
dispatcher: D,
store: D::Store,
// to be used with async actions
#[allow(dead_code)]
action_sender: ActionSender<D::Actions>,
action_receiver: ActionReceiver<D::Actions>,
state_update_sender: StateUpdateSender<D::Store>,
cancellation_token: CancellationToken,
}
impl<D> DispatcherLoop<D>
where
D: ActionDispatcher + Send + Sync + 'static,
D::Store: Send + Sync + 'static,
{
pub(crate) fn new(
dispatcher: D,
store: D::Store,
action_sender: impl Into<ActionSender<D::Actions>>,
action_receiver: impl Into<ActionReceiver<D::Actions>>,
state_update_sender: StateUpdateSender<D::Store>,
cancellation_token: CancellationToken,
) -> DispatcherLoop<D> {
DispatcherLoop {
dispatcher,
store,
action_sender: action_sender.into(),
action_receiver: action_receiver.into(),
state_update_sender,
cancellation_token,
}
}
async fn handle_action(&mut self, action: Option<Action<D::Actions>>) -> eyre::Result<()> {
let Some(action) = action else {
warn!("the dispatcher channel has closed! we're probably already in shutdown!");
// but if we're not, make sure to kick it off...
self.cancellation_token.cancel();
return Ok(());
};
match action {
Action::Quit => {
debug!("attempting to handle the QUIT action");
self.cancellation_token.cancel();
// no need to send any state updates here
return Ok(());
}
Action::AppDefined(action) => {
debug!("attempting to handle the following action: {:?}", action);
self.dispatcher
.handle_app_action(action, &mut self.store)
.await?;
}
}
self.state_update_sender.send(self.store.clone())?;
Ok(())
}
pub async fn run(&mut self) -> eyre::Result<()> {
info!("starting the dispatcher loop");
// issue initial state
self.state_update_sender.send(self.store.clone())?;
loop {
tokio::select! {
biased;
_ = self.cancellation_token.cancelled() => {
info!("received cancellation token");
break;
}
maybe_action = self.action_receiver.next() => {
self.handle_action(maybe_action).await?
}
}
}
Ok(())
}
}
@@ -0,0 +1,6 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub trait State: Clone {
//
}
+80
View File
@@ -0,0 +1,80 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use color_eyre::eyre;
use crossterm::event::EventStream;
use ratatui::backend::CrosstermBackend as Backend;
use ratatui::crossterm::{
self, cursor,
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
};
use std::ops::{Deref, DerefMut};
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio_stream::Stream;
pub struct TuiHandle {
pub terminal: ratatui::Terminal<Backend<std::io::Stderr>>,
pub crossterm_events: EventStream,
}
impl TuiHandle {
pub fn new() -> Result<TuiHandle, eyre::Error> {
let terminal = ratatui::Terminal::new(Backend::new(std::io::stderr()))?;
let crossterm_events = EventStream::new();
Ok(TuiHandle {
terminal,
crossterm_events,
})
}
pub fn enter(&self) -> Result<(), eyre::Error> {
crossterm::terminal::enable_raw_mode()?;
crossterm::execute!(std::io::stderr(), EnterAlternateScreen, cursor::Hide)?;
Ok(())
}
pub fn exit(&mut self) -> eyre::Result<()> {
if crossterm::terminal::is_raw_mode_enabled()? {
self.terminal.flush()?;
crossterm::execute!(std::io::stderr(), LeaveAlternateScreen, cursor::Show)?;
crossterm::terminal::disable_raw_mode()?;
}
Ok(())
}
}
impl Stream for TuiHandle {
type Item = <EventStream as Stream>::Item;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Pin::new(&mut self.crossterm_events).poll_next(cx)
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.crossterm_events.size_hint()
}
}
impl Deref for TuiHandle {
type Target = ratatui::Terminal<Backend<std::io::Stderr>>;
fn deref(&self) -> &Self::Target {
&self.terminal
}
}
impl DerefMut for TuiHandle {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.terminal
}
}
impl Drop for TuiHandle {
fn drop(&mut self) {
// well. at this point we can't do much, we'll just go straight into the panic handler
#[allow(clippy::expect_used)]
self.exit().expect("failed to teardown the terminal")
}
}
+158
View File
@@ -0,0 +1,158 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::tui::action::Action;
use crate::tui::config::TuiConfig;
use crate::tui::dispatcher::{ActionDispatcher, ActionSender, DispatcherLoop};
use crate::tui::ui::UiEventLoop;
use crate::Component;
use color_eyre::eyre;
use std::time::Duration;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use tokio::time::timeout;
use tokio_util::sync::CancellationToken;
use tokio_util::task::TaskTracker;
use tracing::{error, info};
pub struct TuiManager<C: Component> {
shutdown_grace: Duration,
cancel_grace: Duration,
abort_grace: Duration,
task_tracker: TaskTracker,
cancellation_token: CancellationToken,
dispatcher_handle: JoinHandle<eyre::Result<()>>,
ui_event_loop_handle: JoinHandle<eyre::Result<()>>,
action_sender: ActionSender<C::Actions>,
}
impl<C> TuiManager<C>
where
C: Component + Send + Sync + 'static,
C::State: Send + Sync + 'static,
C::Actions: Send + Sync + Clone + 'static,
{
pub(crate) fn build_new<D>(
config: TuiConfig,
action_dispatcher: D,
initial_state: D::Store,
) -> eyre::Result<TuiManager<C>>
where
D: ActionDispatcher<Store = C::State, Actions = C::Actions> + Send + Sync + 'static,
{
let task_tracker = TaskTracker::new();
let cancellation_token = CancellationToken::new();
let (action_tx, action_rx) = mpsc::unbounded_channel();
let (state_tx, state_rx) = mpsc::unbounded_channel();
let mut dispatcher_loop = DispatcherLoop::new(
action_dispatcher,
initial_state,
action_tx.clone(),
action_rx,
state_tx,
cancellation_token.clone(),
);
let mut ui_event_loop = UiEventLoop::<C>::new(
config.tui.debug.tick_rate,
cancellation_token.clone(),
state_rx,
action_tx.clone(),
)?;
let dispatcher_handle = task_tracker.spawn(async move { dispatcher_loop.run().await });
let ui_event_loop_handle = task_tracker.spawn(async move { ui_event_loop.run().await });
task_tracker.close();
Ok(TuiManager {
shutdown_grace: config.debug.shutdown_grace,
cancel_grace: config.debug.cancel_grace,
abort_grace: config.debug.abort_grace,
task_tracker,
cancellation_token,
dispatcher_handle,
ui_event_loop_handle,
action_sender: action_tx.into(),
})
}
async fn graceful_shutdown(&self) {
// 1. try to send quit action to handle it the most gracefully
self.action_sender.send(Action::Quit);
if timeout(self.shutdown_grace, self.task_tracker.wait())
.await
.is_ok()
{
return;
}
error!("timed out while waiting for graceful shutdown");
// 2. if that doesn't work, issue cancellation token
self.cancellation_token.cancel();
if timeout(self.cancel_grace, self.task_tracker.wait())
.await
.is_ok()
{
return;
}
error!("timed out while attempting to resolve cancellation token shutdown");
// 3. finally go with nuclear option and just abort the tasks
self.dispatcher_handle.abort();
self.ui_event_loop_handle.abort();
if timeout(self.abort_grace, self.task_tracker.wait())
.await
.is_ok()
{
return;
}
error!("somehow we still failed to shutdown our tasks! we might end up in a dirty state... oh well")
}
pub(crate) async fn wait_for_exit_or_signal(&self) {
tokio::select! {
_ = self.task_tracker.wait() => {
// user decided to quit with 'normal' action
}
_ = wait_for_signal() => {
self.graceful_shutdown().await
}
}
}
}
#[cfg(unix)]
#[allow(clippy::expect_used)]
pub 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() => {
info!("Received SIGINT");
},
_ = sigterm.recv() => {
info!("Received SIGTERM");
}
_ = sigquit.recv() => {
info!("Received SIGQUIT");
}
}
}
#[cfg(not(unix))]
pub async fn wait_for_signal() {
tokio::select! {
_ = tokio::signal::ctrl_c() => {
info!("Received SIGINT");
},
}
}
+46
View File
@@ -0,0 +1,46 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::tui::handle::TuiHandle;
use color_eyre::eyre;
use tracing::error;
pub mod action;
pub mod config;
pub(crate) mod dispatcher;
pub mod handle;
pub(crate) mod manager;
pub(crate) mod ui;
pub fn initialize_panic_handler() -> eyre::Result<()> {
let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default()
.display_location_section(true)
.display_env_section(true)
.into_hooks();
eyre_hook.install()?;
std::panic::set_hook(Box::new(move |panic_info| {
if let Err(r) = TuiHandle::new().and_then(|mut h| h.exit()) {
error!("Unable to exit Terminal: {:?}", r);
}
let msg = format!("{}", panic_hook.panic_report(panic_info));
#[cfg(not(debug_assertions))]
{
eprintln!("{}", msg); // prints color-eyre stack trace to stderr
}
error!("error: {}", strip_ansi_escapes::strip_str(msg));
#[cfg(debug_assertions)]
{
// Better Panic stacktrace that is only enabled when debugging.
better_panic::Settings::auto()
.most_recent_first(false)
.lineno_suffix(true)
.verbosity(better_panic::Verbosity::Full)
.create_panic_handler()(panic_info);
}
std::process::exit(1);
}));
Ok(())
}
@@ -0,0 +1,70 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::tui::config::keybindings::key_event_to_string;
use crate::tui::dispatcher::ActionSender;
use crate::{AppAction, Component, State};
use crossterm::event::KeyEvent;
use ratatui::layout::{Alignment, Rect};
use ratatui::prelude::{Line, Style, Stylize};
use ratatui::widgets::Block;
use ratatui::Frame;
use std::marker::PhantomData;
pub struct DebugHistory<S, A> {
last_tick_key_events: Vec<KeyEvent>,
phantom_state: PhantomData<S>,
phantom_action: PhantomData<A>,
}
impl<S, A> Component for DebugHistory<S, A>
where
S: State,
A: AppAction,
{
type State = S;
type Actions = A;
fn new(_state: &Self::State, _action_sender: ActionSender<Self::Actions>) -> Self
where
Self: Sized,
{
DebugHistory {
last_tick_key_events: vec![],
phantom_state: PhantomData,
phantom_action: PhantomData,
}
}
fn tick(&mut self) -> bool {
let was_empty = self.last_tick_key_events.is_empty();
self.last_tick_key_events.drain(..);
!was_empty
}
fn handle_key(&mut self, key: KeyEvent) -> color_eyre::Result<()> {
self.last_tick_key_events.push(key);
Ok(())
}
fn view(&mut self, frame: &mut Frame, rect: Rect) {
frame.render_widget(
Block::default()
.title_top(
Line::from(format!(
"{:?}",
&self
.last_tick_key_events
.iter()
.map(key_event_to_string)
.collect::<Vec<_>>()
))
.alignment(Alignment::Right),
)
.title_style(Style::default().bold()),
rect,
);
}
}
@@ -0,0 +1,81 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::tui::config::keybindings::LoggerKeybindings;
use crate::tui::dispatcher::store::State;
use crate::tui::dispatcher::ActionSender;
use crate::tui::ui::components::Component;
use crate::AppAction;
use crossterm::event::KeyEvent;
use ratatui::layout::Rect;
use ratatui::prelude::*;
use ratatui::widgets::Block;
use ratatui::Frame;
use std::marker::PhantomData;
use tui_logger::{TuiLoggerLevelOutput, TuiLoggerSmartWidget, TuiWidgetState};
pub struct Props {
pub keybindings: LoggerKeybindings,
}
pub struct Logger<S, A> {
props: Props,
widget_state: TuiWidgetState,
phantom_state: PhantomData<S>,
phantom_action: PhantomData<A>,
}
impl<S, A> Component for Logger<S, A>
where
S: State,
for<'a> Props: From<&'a S>,
A: AppAction,
{
type State = S;
type Actions = A;
fn new(state: &Self::State, _action_sender: ActionSender<Self::Actions>) -> Self
where
Self: Sized,
{
Logger {
props: Props::from(state),
widget_state: TuiWidgetState::new(),
phantom_state: PhantomData,
phantom_action: PhantomData,
}
}
fn tick(&mut self) -> bool {
true
}
fn handle_key(&mut self, key: KeyEvent) -> color_eyre::Result<()> {
if let Some(tui_event) = self.props.keybindings.tui_logger_event(key.into()) {
self.widget_state.transition(tui_event)
}
Ok(())
}
fn view(&mut self, frame: &mut Frame, rect: Rect) {
let border = Block::bordered();
let inner_area = border.inner(rect);
frame.render_widget(border, rect);
let tui_sm = TuiLoggerSmartWidget::default()
.style_error(Style::default().fg(Color::Red))
.style_warn(Style::default().fg(Color::Yellow))
.style_info(Style::default().fg(Color::Green))
.style_debug(Style::default().fg(Color::Cyan))
.style_trace(Style::default().fg(Color::Magenta))
.output_separator(':')
.output_timestamp(Some("%F %H:%M:%S%.3f".to_string()))
.output_level(Some(TuiLoggerLevelOutput::Long))
.output_target(true)
.output_file(true)
.output_line(true)
.state(&self.widget_state);
frame.render_widget(tui_sm, inner_area);
}
}
@@ -0,0 +1,12 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod debug_history;
#[cfg(feature = "logger")]
pub mod logger;
pub use debug_history::DebugHistory;
#[cfg(feature = "logger")]
pub use logger::{Logger, Props as LoggerProps};
@@ -0,0 +1,45 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::tui::dispatcher::ActionSender;
use crate::{AppAction, State};
use color_eyre::eyre;
use crossterm::event::KeyEvent;
use ratatui::prelude::Rect;
use ratatui::Frame;
pub mod common;
// pub trait Props<'a>: From<&'a Self::State> {
// type State: State;
// }
pub trait Component {
type State: State;
type Actions: AppAction;
fn new(state: &Self::State, action_sender: ActionSender<Self::Actions>) -> Self
where
Self: Sized;
fn update(self, state: &Self::State) -> Self
where
Self: Sized,
{
let _ = state;
self
}
// returns boolean indicating whether a rerender is needed
// fn tick(&mut self) -> bool;
fn tick(&mut self) -> bool {
false
}
fn handle_key(&mut self, key: KeyEvent) -> eyre::Result<()> {
let _ = key;
Ok(())
}
fn view(&mut self, frame: &mut Frame, rect: Rect);
}
+154
View File
@@ -0,0 +1,154 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::tui::dispatcher::{ActionSender, StateUpdateReceiver};
use crate::tui::handle::TuiHandle;
use crate::tui::ui::components::Component;
use color_eyre::eyre;
use color_eyre::eyre::eyre;
use crossterm::event::Event;
use humantime_serde::re::humantime;
use std::io;
use std::marker::PhantomData;
use std::time::Duration;
use tokio::time::{timeout, Instant};
use tokio_stream::StreamExt;
use tokio_util::sync::CancellationToken;
use tracing::{info, trace, warn};
pub mod components;
pub struct UiEventLoop<C: Component> {
tick_rate: Duration,
cancellation_token: CancellationToken,
state_receiver: StateUpdateReceiver<C::State>,
// only to be used to construct root 'App' instance
action_sender: ActionSender<C::Actions>,
tui_handle: TuiHandle,
root_component: PhantomData<C>,
}
impl<C> UiEventLoop<C>
where
C: Component,
{
pub fn new(
tick_rate: Duration,
cancellation_token: CancellationToken,
state_receiver: impl Into<StateUpdateReceiver<C::State>>,
action_sender: impl Into<ActionSender<C::Actions>>,
) -> eyre::Result<Self> {
Ok(UiEventLoop {
tick_rate,
cancellation_token,
state_receiver: state_receiver.into(),
action_sender: action_sender.into(),
tui_handle: TuiHandle::new()?,
root_component: PhantomData,
})
}
async fn handle_ui_event(
&mut self,
event: Option<io::Result<Event>>,
root_app: &mut C,
) -> eyre::Result<()> {
let Some(event) = event else {
warn!("the crossterm event channel has closed! we're probably already in shutdown!");
// but if we're not, make sure to kick it off...
self.cancellation_token.cancel();
return Ok(());
};
match event? {
Event::FocusGained => {}
Event::FocusLost => {}
Event::Key(key_event) => root_app.handle_key(key_event)?,
Event::Mouse(_) => {}
Event::Paste(_) => {}
Event::Resize(_, _) => {}
}
Ok(())
}
async fn handle_updated_state(&mut self, state: Option<C::State>, root_app: C) -> C
where
C: Component,
{
let Some(updated_state) = state else {
warn!("the state update channel has closed! we're probably already in shutdown!");
// but if we're not, make sure to kick it off...
self.cancellation_token.cancel();
return root_app;
};
root_app.update(&updated_state)
}
pub async fn run(&mut self) -> eyre::Result<()>
where
// this clone shouldn't really be needed...
C::Actions: Clone,
{
info!("starting the ui loop");
// wait for initial state...
let initial_state = timeout(Duration::from_secs(1), self.state_receiver.next())
.await?
.ok_or_else(|| eyre!("did not receive initial state!"))?;
let mut root_app = C::new(&initial_state, self.action_sender.clone());
let mut tick_rate = self.tick_rate;
let mut tick_interval = tokio::time::interval(tick_rate);
self.tui_handle.enter()?;
let mut draw = true;
loop {
if draw {
let draw_start = Instant::now();
trace!("redrawing the UI");
self.tui_handle
.draw(|frame| root_app.view(frame, frame.area()))?;
let taken = humantime::format_duration(draw_start.elapsed()).to_string();
trace!(time_taken = taken, "UI drawing");
}
tokio::select! {
biased;
_ = self.cancellation_token.cancelled() => {
info!("received cancellation token");
break;
}
maybe_ui_event = self.tui_handle.next() => {
self.handle_ui_event(maybe_ui_event, &mut root_app).await?;
draw = true;
}
state_update = self.state_receiver.next() => {
root_app = self.handle_updated_state(state_update, root_app).await;
// the tick rate has changed
if self.tick_rate != tick_rate {
tick_rate = self.tick_rate;
tick_interval = tokio::time::interval(tick_rate);
}
draw = true;
}
_ = tick_interval.tick() => {
let tick_start = Instant::now();
draw = root_app.tick();
let taken = humantime::format_duration(tick_start.elapsed()).to_string();
trace!(time_taken = taken, will_redraw = draw, "app tick");
},
}
}
Ok(())
}
}
@@ -3,7 +3,7 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[repr(u8)]
pub enum ServiceProviderType {
NetworkRequester = 0,
+2
View File
@@ -15,11 +15,13 @@ log = { workspace = true }
sysinfo = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
sha2 = { workspace = true }
thiserror = { workspace = true }
time = { workspace = true }
tokio = { workspace = true }
si-scale = { workspace = true }
nym-crypto = { path = "../crypto" }
nym-sphinx = { path = "../nymsphinx" }
nym-credentials-interface = { path = "../credentials-interface" }
nym-metrics = { path = "../nym-metrics" }
@@ -0,0 +1,67 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::ClientStatsEvents;
use nym_credentials_interface::TicketType;
use serde::{Deserialize, Serialize};
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ConnectionStats {
//tickets
mixnet_entry_spent: u32,
vpn_entry_spent: u32,
mixnet_exit_spent: u32,
vpn_exit_spent: u32,
//country_connection
wg_exit_country_code: String,
mix_exit_country_code: String,
}
/// Event space for Nym API statistics tracking
#[derive(Debug)]
pub enum ConnectionStatsEvent {
/// ecash ticket was spend
TicketSpent {
typ: TicketType,
amount: u32,
},
WgCountry(String),
MixCountry(String),
}
impl From<ConnectionStatsEvent> for ClientStatsEvents {
fn from(event: ConnectionStatsEvent) -> ClientStatsEvents {
ClientStatsEvents::Connection(event)
}
}
/// Nym API statistics tracking object
#[derive(Default)]
pub struct ConnectionStatsControl {
// Keep track of packet statistics over time
stats: ConnectionStats,
}
impl ConnectionStatsControl {
pub(crate) fn handle_event(&mut self, event: ConnectionStatsEvent) {
match event {
ConnectionStatsEvent::TicketSpent { typ, amount } => match typ {
TicketType::V1MixnetEntry => self.stats.mixnet_entry_spent += amount,
TicketType::V1MixnetExit => self.stats.mixnet_exit_spent += amount,
TicketType::V1WireguardEntry => self.stats.vpn_entry_spent += amount,
TicketType::V1WireguardExit => self.stats.vpn_exit_spent += amount,
},
ConnectionStatsEvent::WgCountry(cc) => {
self.stats.wg_exit_country_code = cc;
}
ConnectionStatsEvent::MixCountry(cc) => {
self.stats.mix_exit_country_code = cc;
}
}
}
pub(crate) fn report(&self) -> ConnectionStats {
self.stats.clone()
}
}
+10
View File
@@ -16,6 +16,8 @@ pub mod nym_api_statistics;
/// Packet count based statistics.
pub mod packet_statistics;
pub mod connection;
/// Channel receiving generic stats events to be used by a statistics aggregator.
pub type ClientStatsReceiver = tokio::sync::mpsc::UnboundedReceiver<ClientStatsEvents>;
@@ -49,6 +51,8 @@ pub enum ClientStatsEvents {
GatewayConn(gateway_conn_statistics::GatewayStatsEvent),
/// Nym API connection events
NymApi(nym_api_statistics::NymApiStatsEvent),
/// Credential events
Connection(connection::ConnectionStatsEvent),
}
/// Controls stats event handling and reporting
@@ -63,6 +67,7 @@ pub struct ClientStatsController {
packet_stats: packet_statistics::PacketStatisticsControl,
gateway_conn_stats: gateway_conn_statistics::GatewayStatsControl,
nym_api_stats: nym_api_statistics::NymApiStatsControl,
connection_stats: connection::ConnectionStatsControl,
}
impl ClientStatsController {
@@ -76,6 +81,7 @@ impl ClientStatsController {
packet_stats: Default::default(),
gateway_conn_stats: Default::default(),
nym_api_stats: Default::default(),
connection_stats: Default::default(),
}
}
/// Returns a static ClientStatsReport that can be sent somewhere
@@ -88,6 +94,8 @@ impl ClientStatsController {
packet_stats: self.packet_stats.report(),
gateway_conn_stats: self.gateway_conn_stats.report(),
nym_api_stats: self.nym_api_stats.report(),
connection_stats: self.connection_stats.report(),
..Default::default()
}
}
@@ -97,6 +105,7 @@ impl ClientStatsController {
ClientStatsEvents::PacketStatistics(event) => self.packet_stats.handle_event(event),
ClientStatsEvents::GatewayConn(event) => self.gateway_conn_stats.handle_event(event),
ClientStatsEvents::NymApi(event) => self.nym_api_stats.handle_event(event),
ClientStatsEvents::Connection(event) => self.connection_stats.handle_event(event),
}
}
@@ -106,6 +115,7 @@ impl ClientStatsController {
pub fn reset(&mut self) {
self.nym_api_stats = Default::default();
self.gateway_conn_stats = Default::default();
self.connection_stats = Default::default();
//no periodic reset for packet stats
self.last_update_time = ClientStatsController::get_update_time();
+17
View File
@@ -11,6 +11,9 @@
#![warn(clippy::todo)]
#![warn(clippy::dbg_macro)]
use nym_crypto::asymmetric::ed25519;
use sha2::Digest;
/// Client specific statistics interfaces and events.
pub mod clients;
/// Statistics related errors.
@@ -19,3 +22,17 @@ pub mod error;
pub mod gateways;
/// Statistics reporting abstractions and implementations.
pub mod report;
const CLIENT_ID_PREFIX: &str = "client_stats_id";
pub fn generate_client_stats_id(id_key: ed25519::PublicKey) -> String {
generate_stats_id(CLIENT_ID_PREFIX, id_key.to_base58_string())
}
fn generate_stats_id<M: AsRef<[u8]>>(prefix: &str, id_seed: M) -> String {
let mut hasher = sha2::Sha256::new();
hasher.update(prefix);
hasher.update(&id_seed);
let output = hasher.finalize();
format!("{:x}", output)
}
+25 -2
View File
@@ -2,8 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
use crate::clients::{
gateway_conn_statistics::GatewayStats, nym_api_statistics::NymApiStats,
packet_statistics::PacketStatistics,
connection::ConnectionStats, gateway_conn_statistics::GatewayStats,
nym_api_statistics::NymApiStats, packet_statistics::PacketStatistics,
};
use super::error::StatsError;
@@ -12,9 +12,14 @@ use serde::{Deserialize, Serialize};
use sysinfo::System;
use time::OffsetDateTime;
const KIND: &str = "client_stats_report";
const VERSION: &str = "v1";
/// Report object containing both data to be reported and client / device context. We take extra care not to overcapture context information.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ClientStatsReport {
pub(crate) kind: String,
pub(crate) api_version: String,
pub(crate) last_update_time: OffsetDateTime,
pub(crate) client_id: String,
pub(crate) client_type: String,
@@ -22,6 +27,7 @@ pub struct ClientStatsReport {
pub(crate) packet_stats: PacketStatistics,
pub(crate) gateway_conn_stats: GatewayStats,
pub(crate) nym_api_stats: NymApiStats,
pub(crate) connection_stats: ConnectionStats,
}
impl From<ClientStatsReport> for Vec<u8> {
@@ -40,6 +46,23 @@ impl TryFrom<&[u8]> for ClientStatsReport {
}
}
impl Default for ClientStatsReport {
fn default() -> Self {
ClientStatsReport {
kind: KIND.to_string(),
api_version: VERSION.to_string(),
last_update_time: OffsetDateTime::now_utc(),
client_id: Default::default(),
client_type: Default::default(),
os_information: Default::default(),
packet_stats: Default::default(),
gateway_conn_stats: Default::default(),
nym_api_stats: Default::default(),
connection_stats: Default::default(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OsInformation {
pub(crate) os_type: String,
+26
View File
@@ -0,0 +1,26 @@
[package]
name = "nym-ticketbooks-merkle"
version = "0.1.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
readme.workspace = true
[dependencies]
sha2 = { workspace = true }
rs_merkle = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true, features = ["derive"] }
time = { workspace = true }
nym-credentials-interface = { path = "../credentials-interface" }
nym-serde-helpers = { path = "../serde-helpers", features = ["date", "base64", "hex"] }
[dev-dependencies]
rand_chacha = { workspace = true }
rand = { workspace = true }
serde_json = { workspace = true }
+366
View File
@@ -0,0 +1,366 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![warn(clippy::expect_used)]
#![warn(clippy::unwrap_used)]
#![warn(clippy::todo)]
#![warn(clippy::dbg_macro)]
use nym_credentials_interface::TicketType;
use rs_merkle::algorithms::Sha256;
use rs_merkle::{MerkleProof, MerkleTree};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use sha2::Digest;
use std::fmt::{Debug, Formatter};
use time::Date;
// no point in importing the entire contract commons just for this one type
pub type DepositId = u32;
pub type DKGEpochId = u64;
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct IssuedTicketbook {
pub deposit_id: DepositId,
pub epoch_id: DKGEpochId,
// 96 bytes serialised 'BlindedSignature'
#[schemars(with = "String")]
#[serde(with = "nym_serde_helpers::base64")]
pub blinded_partial_credential: Vec<u8>,
// concatenated bytes for the commitments to the private attributes
#[schemars(with = "String")]
#[serde(with = "nym_serde_helpers::base64")]
pub joined_encoded_private_attributes_commitments: Vec<u8>,
#[schemars(with = "String")]
#[serde(with = "nym_serde_helpers::date")]
pub expiration_date: Date,
#[schemars(with = "String")]
pub ticketbook_type: TicketType,
}
impl IssuedTicketbook {
pub fn hash_to_merkle_leaf(&self) -> [u8; 32] {
let mut hasher = sha2::Sha256::new();
hasher.update(self.deposit_id.to_be_bytes());
hasher.update(self.epoch_id.to_be_bytes());
hasher.update(&self.blinded_partial_credential);
hasher.update(&self.joined_encoded_private_attributes_commitments);
hasher.update(self.expiration_date.to_julian_day().to_be_bytes());
hasher.update(self.ticketbook_type.encode().to_be_bytes());
hasher.finalize().into()
}
pub fn signable_plaintext(&self) -> Vec<u8> {
self.deposit_id
.to_be_bytes()
.into_iter()
.chain(self.epoch_id.to_be_bytes())
.chain(self.blinded_partial_credential.iter().copied())
.chain(
self.joined_encoded_private_attributes_commitments
.iter()
.copied(),
)
.chain(self.expiration_date.to_julian_day().to_be_bytes())
.chain(self.ticketbook_type.encode().to_be_bytes())
.collect()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InsertedMerkleLeaf {
#[serde(with = "nym_serde_helpers::hex")]
pub new_root: Vec<u8>,
pub leaf: MerkleLeaf,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialOrd, PartialEq, Eq)]
pub struct MerkleLeaf {
#[schemars(with = "String")]
#[serde(with = "nym_serde_helpers::hex")]
pub hash: Vec<u8>,
pub index: usize,
}
#[derive(Default, Clone)]
pub struct IssuedTicketbooksMerkleTree {
inner: MerkleTree<Sha256>,
}
impl IssuedTicketbooksMerkleTree {
pub fn new() -> IssuedTicketbooksMerkleTree {
IssuedTicketbooksMerkleTree {
inner: MerkleTree::new(),
}
}
pub fn rebuild(leaves: &[[u8; 32]]) -> IssuedTicketbooksMerkleTree {
IssuedTicketbooksMerkleTree {
inner: MerkleTree::from_leaves(leaves),
}
}
pub fn all_leaves(&self) -> Option<Vec<[u8; 32]>> {
self.inner.leaves()
}
pub fn insert(&mut self, issued: &IssuedTicketbook) -> InsertedMerkleLeaf {
let hash = issued.hash_to_merkle_leaf();
self.insert_leaf(hash)
}
#[allow(clippy::unwrap_used)]
pub fn insert_leaf(&mut self, leaf_hash: [u8; 32]) -> InsertedMerkleLeaf {
let leaves = self.inner.leaves_len();
self.inner.insert(leaf_hash).commit();
InsertedMerkleLeaf {
// SAFETY: after inserting at least a single node, the root will always be available
new_root: self.inner.root().unwrap().to_vec(),
leaf: MerkleLeaf {
hash: leaf_hash.to_vec(),
index: leaves,
},
}
}
pub fn rollback(&mut self) {
self.inner.rollback();
}
pub fn root(&self) -> Option<[u8; 32]> {
self.inner.root()
}
pub fn generate_proof(
&self,
leaf_indices: &[usize],
) -> Option<IssuedTicketbooksFullMerkleProof> {
let leaves = self.inner.leaves()?;
let mut included_leaves = Vec::new();
for &index in leaf_indices {
let hash = *leaves.get(index)?;
included_leaves.push(MerkleLeaf {
hash: hash.to_vec(),
index,
})
}
Some(IssuedTicketbooksFullMerkleProof {
inner_proof: self.inner.proof(leaf_indices),
included_leaves,
total_leaves: self.inner.leaves_len(),
root: self.inner.root()?.to_vec(),
})
}
}
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct IssuedTicketbooksFullMerkleProof {
#[schemars(with = "String")]
#[serde(with = "inner_proof_base64_serde")]
inner_proof: MerkleProof<Sha256>,
included_leaves: Vec<MerkleLeaf>,
total_leaves: usize,
#[schemars(with = "String")]
#[serde(with = "nym_serde_helpers::hex")]
root: Vec<u8>,
}
impl Debug for IssuedTicketbooksFullMerkleProof {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("IssuedTicketbooksFullMerkleProof")
.field("inner_proof", &self.inner_proof.proof_hashes_hex())
.field("included_leaves", &self.included_leaves)
.field("total_leaves", &self.total_leaves)
.field("root", &self.root)
.finish()
}
}
impl Clone for IssuedTicketbooksFullMerkleProof {
fn clone(&self) -> Self {
IssuedTicketbooksFullMerkleProof {
inner_proof: MerkleProof::new(self.inner_proof.proof_hashes().to_vec()),
included_leaves: self.included_leaves.clone(),
total_leaves: self.total_leaves,
root: self.root.clone(),
}
}
}
impl IssuedTicketbooksFullMerkleProof {
pub fn contains_leaf_hash(&self, hash: [u8; 32]) -> bool {
self.included_leaves.iter().any(|m| m.hash == hash)
}
pub fn contains_full_leaf(&self, leaf: &MerkleLeaf) -> bool {
self.included_leaves.iter().any(|m| m == leaf)
}
pub fn total_leaves(&self) -> usize {
self.total_leaves
}
pub fn verify(&self, expected_root: [u8; 32]) -> bool {
if self.root != expected_root {
return false;
}
let mut leaf_indices = Vec::with_capacity(self.included_leaves.len());
let mut leaf_hashes = Vec::with_capacity(self.included_leaves.len());
for leaf in &self.included_leaves {
leaf_indices.push(leaf.index);
let Ok(sha256_hash) = leaf.hash.clone().try_into() else {
return false;
};
leaf_hashes.push(sha256_hash);
}
self.inner_proof.verify(
expected_root,
&leaf_indices,
&leaf_hashes,
self.total_leaves,
)
}
}
mod inner_proof_base64_serde {
use rs_merkle::algorithms::Sha256;
use rs_merkle::proof_serializers::DirectHashesOrder;
use rs_merkle::MerkleProof;
use serde::{Deserializer, Serializer};
pub fn serialize<S: Serializer>(
proof: &MerkleProof<Sha256>,
serializer: S,
) -> Result<S::Ok, S::Error> {
let bytes = proof.serialize::<DirectHashesOrder>();
nym_serde_helpers::base64::serialize(&bytes, serializer)
}
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<MerkleProof<Sha256>, D::Error> {
let bytes = nym_serde_helpers::base64::deserialize(deserializer)?;
MerkleProof::<Sha256>::deserialize::<DirectHashesOrder>(&bytes)
.map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
use nym_credentials_interface::ecash_today;
use rand::{RngCore, SeedableRng};
fn test_rng() -> rand_chacha::ChaChaRng {
let dummy_seed = [42u8; 32];
rand_chacha::ChaCha20Rng::from_seed(dummy_seed)
}
fn dummy_issued(rng: &mut rand_chacha::ChaCha20Rng) -> IssuedTicketbook {
let mut blinded_partial_credential = vec![0u8; 42];
rng.fill_bytes(&mut blinded_partial_credential);
let mut joined_encoded_private_attributes_commitments = vec![0u8; 48 * 3];
rng.fill_bytes(&mut joined_encoded_private_attributes_commitments);
IssuedTicketbook {
deposit_id: rng.next_u32(),
epoch_id: rng.next_u64(),
blinded_partial_credential,
joined_encoded_private_attributes_commitments,
expiration_date: ecash_today().date(),
ticketbook_type: TicketType::V1MixnetEntry,
}
}
#[test]
fn single_leaf() {
let mut rng = test_rng();
let issued = dummy_issued(&mut rng);
let expected_hash = issued.hash_to_merkle_leaf();
let mut tree = IssuedTicketbooksMerkleTree::new();
let inserted_node = tree.insert(&issued);
assert_eq!(inserted_node.leaf.index, 0);
assert_eq!(inserted_node.leaf.hash, expected_hash);
assert_eq!(inserted_node.new_root, expected_hash);
let proof = tree.generate_proof(&[0]).unwrap();
assert!(proof.verify(expected_hash));
assert_eq!(proof.total_leaves, 1);
assert_eq!(proof.included_leaves, vec![inserted_node.leaf]);
assert_eq!(proof.root, expected_hash);
}
#[test]
fn multiple_leaves() {
let mut rng = test_rng();
let mut tree = IssuedTicketbooksMerkleTree::new();
for i in 0..100 {
let issued = dummy_issued(&mut rng);
let expected_hash = issued.hash_to_merkle_leaf();
let inserted_node = tree.insert(&issued);
assert_eq!(inserted_node.leaf.index, i);
assert_eq!(inserted_node.leaf.hash, expected_hash);
// proof for this single node
let proof = tree.generate_proof(&[i]).unwrap();
assert!(proof.verify(tree.root().unwrap()));
assert_eq!(proof.total_leaves, i + 1);
assert!(proof.contains_leaf_hash(expected_hash));
}
// proof for multiple nodes
let indices = [0, 5, 42, 69, 74, 99];
let all_leaves = tree.inner.leaves().unwrap();
let big_proof = tree.generate_proof(&indices).unwrap();
for &index in &indices {
let leaf_hash = all_leaves.get(index).unwrap();
assert!(big_proof.contains_leaf_hash(*leaf_hash));
}
assert!(big_proof.verify(tree.root().unwrap()))
}
#[test]
fn merkle_proof_serialisation_roundtrip() {
let mut rng = test_rng();
let mut tree = IssuedTicketbooksMerkleTree::new();
for _ in 0..100 {
let issued = dummy_issued(&mut rng);
tree.insert(&issued);
}
let indices = [0, 5, 42, 69, 74, 99];
let big_proof = tree.generate_proof(&indices).unwrap();
let bytes = serde_json::to_vec(&big_proof).unwrap();
let recovered: IssuedTicketbooksFullMerkleProof = serde_json::from_slice(&bytes).unwrap();
assert_eq!(
big_proof.inner_proof.proof_hashes(),
recovered.inner_proof.proof_hashes()
);
assert_eq!(big_proof.included_leaves, recovered.included_leaves);
assert_eq!(big_proof.total_leaves, recovered.total_leaves);
assert_eq!(big_proof.root, recovered.root);
}
}
+9 -1
View File
@@ -3,7 +3,7 @@ use crate::deprecated::DelegationEvent;
use crate::error::TypesError;
use crate::mixnode::NodeCostParams;
use cosmwasm_std::Decimal;
use nym_mixnet_contract_common::{Delegation as MixnetContractDelegation, NodeId};
use nym_mixnet_contract_common::{Delegation as MixnetContractDelegation, NodeId, NodeRewarding};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -70,6 +70,14 @@ pub struct DelegationWithEverything {
pub mixnode_is_unbonding: Option<bool>,
}
pub struct NodeInformation {
pub owner: String,
pub mix_id: NodeId,
pub node_identity: String,
pub rewarding_details: NodeRewarding,
pub is_unbonding: bool,
}
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
+1
View File
@@ -30,6 +30,7 @@ nym-crypto = { path = "../../crypto", features = ["asymmetric", "serde"] }
nym-gateway-client = { path = "../../client-libs/gateway-client", default-features = false, features = ["wasm"] }
nym-sphinx = { path = "../../nymsphinx" }
nym-sphinx-acknowledgements = { path = "../../nymsphinx/acknowledgements", features = ["serde"]}
nym-statistics-common = { path = "../../statistics" }
nym-task = { path = "../../task" }
nym-topology = { path = "../../topology", features = ["serializable", "wasm-serde-types"] }
nym-validator-client = { path = "../../client-libs/validator-client", default-features = false }
+1
View File
@@ -26,6 +26,7 @@ pub use nym_sphinx::{
params::PacketType,
receiver::ReconstructedMessage,
};
pub use nym_statistics_common::clients::ClientStatsSender;
pub use nym_task;
pub use nym_topology::{HardcodedTopologyProvider, MixLayer, NymTopology, TopologyProvider};
pub use nym_validator_client::nym_api::Client as ApiClient;
+7 -3
View File
@@ -7,8 +7,8 @@ use defguard_wireguard_rs::{
WireguardInterfaceApi,
};
use futures::channel::oneshot;
use nym_authenticator_requests::{
latest::registration::RemainingBandwidthData, v1::registration::BANDWIDTH_CAP_PER_DAY,
use nym_authenticator_requests::latest::registration::{
RemainingBandwidthData, BANDWIDTH_CAP_PER_DAY,
};
use nym_credential_verification::{
bandwidth_storage_manager::BandwidthStorageManager, BandwidthFlushingBehaviourConfig,
@@ -194,6 +194,10 @@ impl<St: Storage + Clone + 'static> PeerController<St> {
);
self.bw_storage_managers
.insert(peer.public_key.clone(), bandwidth_storage_manager);
// try to immediately update the host information, to eliminate races
if let Ok(host_information) = self.wg_api.inner.read_interface_data() {
*self.host_information.write().await = host_information;
}
tokio::spawn(async move {
if let Err(e) = handle.run().await {
log::error!("Peer handle shut down ungracefully - {e}");
@@ -230,7 +234,7 @@ impl<St: Storage + Clone + 'static> PeerController<St> {
// host information not updated yet
return Ok(None);
};
BANDWIDTH_CAP_PER_DAY.saturating_sub((peer.rx_bytes + peer.tx_bytes) as i64)
BANDWIDTH_CAP_PER_DAY.saturating_sub(peer.rx_bytes + peer.tx_bytes) as i64
};
Ok(Some(RemainingBandwidthData {
+3 -3
View File
@@ -6,7 +6,7 @@ use crate::peer_controller::PeerControlRequest;
use defguard_wireguard_rs::host::Peer;
use defguard_wireguard_rs::{host::Host, key::Key};
use futures::channel::oneshot;
use nym_authenticator_requests::v2::registration::BANDWIDTH_CAP_PER_DAY;
use nym_authenticator_requests::latest::registration::BANDWIDTH_CAP_PER_DAY;
use nym_credential_verification::bandwidth_storage_manager::BandwidthStorageManager;
use nym_gateway_storage::models::WireguardPeer;
use nym_gateway_storage::Storage;
@@ -18,7 +18,7 @@ use tokio::sync::{mpsc, RwLock};
use tokio_stream::{wrappers::IntervalStream, StreamExt};
pub(crate) type SharedBandwidthStorageManager<St> = Arc<RwLock<BandwidthStorageManager<St>>>;
const AUTO_REMOVE_AFTER: Duration = Duration::from_secs(60 * 60 * 24); // 24 hours
const AUTO_REMOVE_AFTER: Duration = Duration::from_secs(60 * 60 * 24 * 30); // 30 days
pub struct PeerHandle<St> {
storage: St,
@@ -98,7 +98,7 @@ impl<St: Storage + Clone + 'static> PeerHandle<St> {
} else {
if SystemTime::now().duration_since(self.startup_timestamp)? >= AUTO_REMOVE_AFTER {
log::debug!(
"Peer {} has been present for 24 hours, removing it",
"Peer {} has been present for 30 days, removing it",
self.public_key.to_string()
);
let success = self.remove_peer().await?;
+15 -9
View File
@@ -1219,6 +1219,7 @@ dependencies = [
name = "nym-ecash"
version = "0.1.0"
dependencies = [
"anyhow",
"bs58 0.4.0",
"cosmwasm-schema",
"cosmwasm-std",
@@ -1286,6 +1287,7 @@ dependencies = [
"nym-vesting-contract-common",
"rand",
"rand_chacha",
"semver",
"serde",
"thiserror",
"time",
@@ -1302,9 +1304,9 @@ dependencies = [
"cw-storage-plus",
"cw2",
"humantime-serde",
"log",
"nym-contracts-common",
"schemars",
"semver",
"serde",
"serde-json-wasm",
"serde_repr",
@@ -1330,6 +1332,10 @@ dependencies = [
[[package]]
name = "nym-network-defaults"
version = "0.1.0"
dependencies = [
"cargo_metadata",
"regex",
]
[[package]]
name = "nym-pemstore"
@@ -1570,9 +1576,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.10.4"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
@@ -1582,9 +1588,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.6"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
@@ -1593,9 +1599,9 @@ dependencies = [
[[package]]
name = "regex-syntax"
version = "0.8.3"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rfc6979"
@@ -1693,9 +1699,9 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.22"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
dependencies = [
"serde",
]
+1
View File
@@ -36,6 +36,7 @@ nym-multisig-contract-common = { path = "../../common/cosmwasm-smart-contracts/m
nym-network-defaults = { path = "../../common/network-defaults", default-features = false }
[dev-dependencies]
anyhow = { workspace = true }
sylvia = { workspace = true, features = ["mt"] }
nym-crypto = { path = "../../common/crypto", features = ["rand", "asymmetric"] }
rand_chacha = "0.3"
+63
View File
@@ -328,6 +328,19 @@
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"get_latest_deposit"
],
"properties": {
"get_latest_deposit": {
"type": "object",
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
@@ -579,6 +592,56 @@
}
}
},
"get_latest_deposit": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "LatestDepositResponse",
"type": "object",
"properties": {
"deposit": {
"anyOf": [
{
"$ref": "#/definitions/DepositData"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false,
"definitions": {
"Deposit": {
"type": "object",
"required": [
"bs58_encoded_ed25519_pubkey"
],
"properties": {
"bs58_encoded_ed25519_pubkey": {
"type": "string"
}
},
"additionalProperties": false
},
"DepositData": {
"type": "object",
"required": [
"deposit",
"id"
],
"properties": {
"deposit": {
"$ref": "#/definitions/Deposit"
},
"id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
}
},
"get_required_deposit_amount": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Coin",
+13
View File
@@ -88,6 +88,19 @@
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"get_latest_deposit"
],
"properties": {
"get_latest_deposit": {
"type": "object",
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
@@ -0,0 +1,50 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "LatestDepositResponse",
"type": "object",
"properties": {
"deposit": {
"anyOf": [
{
"$ref": "#/definitions/DepositData"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false,
"definitions": {
"Deposit": {
"type": "object",
"required": [
"bs58_encoded_ed25519_pubkey"
],
"properties": {
"bs58_encoded_ed25519_pubkey": {
"type": "string"
}
},
"additionalProperties": false
},
"DepositData": {
"type": "object",
"required": [
"deposit",
"id"
],
"properties": {
"deposit": {
"$ref": "#/definitions/Deposit"
},
"id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
}
}
+195 -2
View File
@@ -2,9 +2,11 @@
// SPDX-License-Identifier: Apache-2.0
use crate::contract::NymEcashContract;
use crate::helpers::{create_batch_redemption_proposal, create_blacklist_proposal, ProposalId};
use crate::helpers::{
create_batch_redemption_proposal, create_blacklist_proposal, Config, ProposalId,
};
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{to_binary, Addr, Deps, Storage, SubMsg};
use cosmwasm_std::{to_binary, Addr, Coin, Decimal, Deps, Storage, SubMsg, Uint128};
use cw3::ProposalResponse;
use nym_ecash_contract_common::EcashContractError;
use nym_multisig_contract_common::msg::QueryMsg as MultisigQueryMsg;
@@ -31,6 +33,28 @@ impl NymEcashContract<'_> {
Ok(TICKETBOOK_SIZE)
}
pub(crate) fn tickets_redemption_amount(
&self,
storage: &dyn Storage,
config: &Config,
number_of_tickets: u16,
) -> Result<Coin, EcashContractError> {
let deposit_amount = config.deposit_amount.amount;
let ticketbook_size = Uint128::new(self.get_ticketbook_size(storage)? as u128);
let tickets = Uint128::new(number_of_tickets as u128);
// how many tickets from a ticketbook you redeemed
let book_ratio = Decimal::from_ratio(tickets, ticketbook_size);
// return = ticketbook_price * (tickets / ticketbook_size)
let return_amount = book_ratio * deposit_amount;
Ok(Coin {
denom: config.deposit_amount.denom.clone(),
amount: return_amount,
})
}
fn must_get_multisig_addr(&self, deps: Deps) -> Result<Addr, EcashContractError> {
// SAFETY: multisig admin MUST always be set on initialisation,
// if the call fails, we're in some weird UB land
@@ -93,3 +117,172 @@ impl NymEcashContract<'_> {
Ok(proposal_response)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::support::tests::TestSetupSimple;
#[test]
fn ticket_redemption_amount() -> anyhow::Result<()> {
// make sure the ticketbook size hasn't changed so that our tests are still valid
assert_eq!(TICKETBOOK_SIZE, 50);
// ticketbook price of 100nym
let test = TestSetupSimple::new().with_deposit_amount(100_000_000);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 1)?;
assert_eq!(res.amount.u128(), 2_000_000);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 2)?;
assert_eq!(res.amount.u128(), 4_000_000);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 5)?;
assert_eq!(res.amount.u128(), 10_000_000);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 10)?;
assert_eq!(res.amount.u128(), 20_000_000);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 30)?;
assert_eq!(res.amount.u128(), 60_000_000);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 50)?;
assert_eq!(res.amount.u128(), 100_000_000);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 123)?;
assert_eq!(res.amount.u128(), 246_000_000);
// ticketbook price of 1.5unym per ticket
let test = TestSetupSimple::new().with_deposit_amount(75);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 1)?;
assert_eq!(res.amount.u128(), 1);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 2)?;
assert_eq!(res.amount.u128(), 3);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 5)?;
assert_eq!(res.amount.u128(), 7);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 10)?;
assert_eq!(res.amount.u128(), 15);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 30)?;
assert_eq!(res.amount.u128(), 45);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 50)?;
assert_eq!(res.amount.u128(), 75);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 123)?;
assert_eq!(res.amount.u128(), 184);
// ticketbook price of 1unym per ticket
let test = TestSetupSimple::new().with_deposit_amount(50);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 1)?;
assert_eq!(res.amount.u128(), 1);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 2)?;
assert_eq!(res.amount.u128(), 2);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 5)?;
assert_eq!(res.amount.u128(), 5);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 10)?;
assert_eq!(res.amount.u128(), 10);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 30)?;
assert_eq!(res.amount.u128(), 30);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 50)?;
assert_eq!(res.amount.u128(), 50);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 123)?;
assert_eq!(res.amount.u128(), 123);
// ticketbook price of 1unym in total
let test = TestSetupSimple::new().with_deposit_amount(1);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 1)?;
assert_eq!(res.amount.u128(), 0);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 2)?;
assert_eq!(res.amount.u128(), 0);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 5)?;
assert_eq!(res.amount.u128(), 0);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 10)?;
assert_eq!(res.amount.u128(), 0);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 30)?;
assert_eq!(res.amount.u128(), 0);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 50)?;
assert_eq!(res.amount.u128(), 1);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 123)?;
assert_eq!(res.amount.u128(), 2);
// ticketbook price of 0unym
let test = TestSetupSimple::new().with_deposit_amount(0);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 1)?;
assert_eq!(res.amount.u128(), 0);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 2)?;
assert_eq!(res.amount.u128(), 0);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 5)?;
assert_eq!(res.amount.u128(), 0);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 10)?;
assert_eq!(res.amount.u128(), 0);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 30)?;
assert_eq!(res.amount.u128(), 0);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 50)?;
assert_eq!(res.amount.u128(), 0);
let res =
test.contract()
.tickets_redemption_amount(test.deps().storage, &test.config(), 123)?;
assert_eq!(res.amount.u128(), 0);
Ok(())
}
}
+29 -20
View File
@@ -8,9 +8,7 @@ use crate::helpers::{
BlacklistKey, Config, MultisigReply, BLACKLIST_PAGE_DEFAULT_LIMIT, BLACKLIST_PAGE_MAX_LIMIT,
CONTRACT_NAME, CONTRACT_VERSION, DEPOSITS_PAGE_DEFAULT_LIMIT, DEPOSITS_PAGE_MAX_LIMIT,
};
use cosmwasm_std::{
coin, BankMsg, Coin, Decimal, Event, Order, Reply, Response, StdResult, Uint128,
};
use cosmwasm_std::{coin, BankMsg, Coin, Event, Order, Reply, Response, StdResult};
use cw4::Cw4Contract;
use cw_controllers::Admin;
use cw_storage_plus::{Bound, Item, Map};
@@ -19,7 +17,9 @@ use nym_ecash_contract_common::blacklist::{
BlacklistedAccount, BlacklistedAccountResponse, Blacklisting, PagedBlacklistedAccountResponse,
};
use nym_ecash_contract_common::counters::PoolCounters;
use nym_ecash_contract_common::deposit::{DepositData, DepositResponse, PagedDepositsResponse};
use nym_ecash_contract_common::deposit::{
DepositData, DepositResponse, LatestDepositResponse, PagedDepositsResponse,
};
use nym_ecash_contract_common::events::{
DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_ID, PROPOSAL_ID_ATTRIBUTE_NAME,
};
@@ -179,6 +179,25 @@ impl NymEcashContract<'_> {
})
}
#[msg(query)]
pub fn get_latest_deposit(
&self,
ctx: QueryCtx,
) -> Result<LatestDepositResponse, EcashContractError> {
let Some(latest_id) = self.deposits.latest_deposit(ctx.deps.storage)? else {
return Ok(LatestDepositResponse::default());
};
let maybe_deposit = self.deposits.try_load_by_id(ctx.deps.storage, latest_id)?;
Ok(LatestDepositResponse {
deposit: maybe_deposit.map(|deposit| DepositData {
id: latest_id,
deposit,
}),
})
}
#[msg(query)]
pub fn get_deposits_paged(
&self,
@@ -283,30 +302,20 @@ impl NymEcashContract<'_> {
.assert_admin(ctx.deps.as_ref(), &ctx.info.sender)?;
let config = self.config.load(ctx.deps.storage)?;
// TODO: we need unit tests for this
let deposit_amount = config.deposit_amount.amount;
let ticketbook_size = Uint128::new(self.get_ticketbook_size(ctx.deps.storage)? as u128);
let tickets = Uint128::new(n as u128);
// how many tickets from a ticketbook you redeemed
let book_ratio = Decimal::from_ratio(tickets, ticketbook_size);
// return = ticketbook_price * (tickets / ticketbook_size)
let return_amount = book_ratio * deposit_amount;
let to_return = self.tickets_redemption_amount(ctx.deps.storage, &config, n)?;
if to_return.amount.is_zero() {
return Ok(Response::new());
}
self.pool_counters
.update(ctx.deps.storage, |mut counters| -> StdResult<_> {
counters.total_redeemed.amount += return_amount;
counters.total_redeemed.amount += to_return.amount;
Ok(counters)
})?;
Ok(Response::new().add_message(BankMsg::Send {
to_address: config.holding_account.to_string(),
amount: vec![Coin {
denom: config.deposit_amount.denom,
amount: return_amount,
}],
amount: vec![to_return],
}))
}
+32
View File
@@ -19,6 +19,15 @@ impl<'a> DepositStorage<'a> {
}
}
pub fn latest_deposit(
&self,
storage: &dyn Storage,
) -> Result<Option<DepositId>, EcashContractError> {
self.deposit_id_counter
.may_load(storage)
.map_err(Into::into)
}
fn next_id(&self, store: &mut dyn Storage) -> Result<DepositId, EcashContractError> {
let id: DepositId = self.deposit_id_counter.may_load(store)?.unwrap_or_default();
let next_id = id + 1;
@@ -107,6 +116,29 @@ mod tests {
use cosmwasm_std::testing::mock_dependencies;
use nym_crypto::asymmetric::ed25519;
#[test]
fn getting_latest_deposit() -> anyhow::Result<()> {
let mut deps = mock_dependencies();
let storage = DepositStorage::new();
// it's `None` for fresh storage
let first = storage.latest_deposit(deps.as_ref().storage)?;
assert!(first.is_none());
let _ = storage.next_id(deps.as_mut().storage)?;
// is correctly incremented for each subsequent id
let second = storage.latest_deposit(deps.as_ref().storage)?;
assert_eq!(second, Some(1));
let _ = storage.next_id(deps.as_mut().storage)?;
let third = storage.latest_deposit(deps.as_ref().storage)?;
assert_eq!(third, Some(2));
Ok(())
}
#[test]
fn iterating_over_deposits() {
let mut deps = mock_dependencies();

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