Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 24fe43b6d5 | |||
| 5cda49f996 | |||
| 36657bcd97 | |||
| 6167243a10 | |||
| 920276f2ac | |||
| 1f53da7456 | |||
| 2fdc0dc47b | |||
| a720c95dd7 | |||
| 013941dbaf | |||
| 1af6e1ecdd | |||
| 379c1eb0d0 | |||
| d1e91946e6 | |||
| 402c79f2f5 | |||
| 149b2f4e32 | |||
| 221e809da3 | |||
| 760ee453ea | |||
| 3f072e4e9d | |||
| aa460076f4 | |||
| b9500aacf3 | |||
| 3651663d1c | |||
| 4d43728059 | |||
| 7b1fbab9af | |||
| f3f8dd35ef | |||
| 7bafe6583a | |||
| 855eecf800 | |||
| b49ef643df | |||
| 62e0771236 | |||
| 05b55a1577 | |||
| b5f1d674fe | |||
| 086b4f6f54 | |||
| 5ad11f2048 | |||
| 99e4ff9132 | |||
| 6dc9b79ace | |||
| 35343b5220 | |||
| e44a36e5b5 | |||
| db20c2e2fa | |||
| 94f247563b | |||
| 827a13523c | |||
| 69718db6d2 | |||
| 475a01c089 | |||
| 01e6a77cf1 | |||
| a348ff43b0 | |||
| 6809f7302e | |||
| bea4eb5cb0 | |||
| fa45b5e564 | |||
| 1a64442d9c | |||
| 46d8206713 | |||
| b8c1014fea | |||
| 43e4224f53 | |||
| d2817d6782 | |||
| e97c94ef9e | |||
| aa919a5351 | |||
| 7617675dcc | |||
| 276925814f | |||
| 46a33b5ef6 | |||
| 532c25c4f5 | |||
| 8f9c26e7a6 | |||
| c0aadebf80 | |||
| 5b216e8d40 | |||
| 4fab7eac3f | |||
| ac77712cc0 | |||
| a400aa8928 | |||
| c001059af9 | |||
| fd8dc63c88 | |||
| d03c5b3650 | |||
| 69e97b3bbc | |||
| 15ca24b848 | |||
| fa551b6d9d | |||
| c6959d3e2d | |||
| 2569deb080 |
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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 }}/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
@@ -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
File diff suppressed because it is too large
Load Diff
+14
-3
@@ -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,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,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,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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
¶ms,
|
||||
)
|
||||
.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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+5
-13
@@ -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
|
||||
}
|
||||
|
||||
+23
-5
@@ -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,
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 },
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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 {
|
||||
//
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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?;
|
||||
|
||||
Generated
+15
-9
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user