Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1266f39446 | |||
| eb59615c56 | |||
| 07c908c497 | |||
| 6de0c4ce92 | |||
| 05d8b31e51 | |||
| 692fbf1392 | |||
| 0de4aea77b | |||
| a7cd8efc04 | |||
| 56aad75220 | |||
| e2f2ab89ec | |||
| 4d09b6f2e5 | |||
| b9339b8f0c | |||
| 43a7360399 | |||
| 5f9f7f0fac | |||
| df0e2fe489 | |||
| bc33cc4c8d | |||
| a31597aca9 | |||
| 378229b04e | |||
| fec196c097 | |||
| 1d7ffc1bb6 | |||
| 0caa627960 | |||
| d6b3d7fc0a | |||
| ac273480f8 | |||
| 79603d61d7 | |||
| e8e9a70ef4 | |||
| 3ac58e0c49 | |||
| e52bd918fb | |||
| 9d82d6d111 | |||
| 3593631e4a | |||
| f5846d5bc2 | |||
| d7779df1b7 | |||
| 7fcc188041 | |||
| b8c8d33c94 | |||
| 02909c03dd | |||
| 11262836d2 | |||
| f26fd5384d | |||
| 085103b333 | |||
| 574f7f1abd | |||
| 31e161604a | |||
| e4e349bea8 | |||
| 6391b7ed3a | |||
| c225511f95 | |||
| 4eedbb235a | |||
| 548b8717b2 | |||
| a215b3d0bf | |||
| 03d5a133eb |
@@ -44,8 +44,10 @@ jobs:
|
||||
echo "Tag is empty"
|
||||
exit 1
|
||||
fi
|
||||
# first, list all tags for logging purposes
|
||||
curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq
|
||||
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq --arg tag $TAG '.tags | contains([$tag])' )
|
||||
# check if there's a matching tag
|
||||
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq -r --arg tag "$TAG" 'any(.tags[]; . == $tag)' )
|
||||
if [[ $exists = "true" ]]; then
|
||||
echo "Version '$TAG' defined in Cargo.toml ALREADY EXISTS as tag in harbor repo"
|
||||
exit 1
|
||||
@@ -53,5 +55,5 @@ jobs:
|
||||
echo "Version '$TAG' doesn't exist on the remote"
|
||||
else
|
||||
echo "Unknown output '$exists'"
|
||||
exit 1
|
||||
exit 2
|
||||
fi
|
||||
|
||||
@@ -44,8 +44,10 @@ jobs:
|
||||
echo "Tag is empty"
|
||||
exit 1
|
||||
fi
|
||||
# first, list all tags for logging purposes
|
||||
curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq
|
||||
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq --arg tag $TAG '.tags | contains([$tag])' )
|
||||
# check if there's a matching tag
|
||||
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq -r --arg tag "$TAG" 'any(.tags[]; . == $tag)' )
|
||||
if [[ $exists = "true" ]]; then
|
||||
echo "Version '$TAG' defined in Cargo.toml ALREADY EXISTS as tag in harbor repo"
|
||||
exit 1
|
||||
@@ -53,5 +55,5 @@ jobs:
|
||||
echo "Version '$TAG' doesn't exist on the remote"
|
||||
else
|
||||
echo "Unknown output '$exists'"
|
||||
exit 1
|
||||
exit 2
|
||||
fi
|
||||
|
||||
@@ -57,6 +57,7 @@ jobs:
|
||||
cp contracts/target/wasm32-unknown-unknown/release/cw4_group.wasm $OUTPUT_DIR
|
||||
cp contracts/target/wasm32-unknown-unknown/release/nym_ecash.wasm $OUTPUT_DIR
|
||||
cp contracts/target/wasm32-unknown-unknown/release/nym_pool_contract.wasm $OUTPUT_DIR
|
||||
cp contracts/target/wasm32-unknown-unknown/release/nym_performance_contract.wasm $OUTPUT_DIR
|
||||
|
||||
- name: Deploy branch to CI www
|
||||
continue-on-error: true
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
name: ci-nym-wallet-storybook
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'nym-wallet/**'
|
||||
- '.github/workflows/ci-nym-wallet-storybook.yml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: custom-linux
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install rsync
|
||||
run: sudo apt-get install rsync
|
||||
continue-on-error: true
|
||||
|
||||
- uses: rlespinasse/github-slug-action@v3.x
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Setup yarn
|
||||
run: npm install -g yarn
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Install wasm-pack
|
||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
|
||||
- name: Build dependencies
|
||||
run: yarn && yarn build
|
||||
|
||||
- name: Build storybook
|
||||
run: yarn storybook:build
|
||||
working-directory: ./nym-wallet
|
||||
|
||||
- name: Deploy branch to CI www (storybook)
|
||||
continue-on-error: true
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
|
||||
ARGS: "-rltgoDzvO --delete"
|
||||
SOURCE: "nym-wallet/storybook-static/"
|
||||
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/wallet-${{ env.GITHUB_REF_SLUG }}
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
|
||||
- name: Matrix - Node Install
|
||||
run: npm install
|
||||
working-directory: .github/workflows/support-files
|
||||
|
||||
- name: Matrix - Send Notification
|
||||
env:
|
||||
NYM_NOTIFICATION_KIND: nym-wallet
|
||||
NYM_PROJECT_NAME: "nym-wallet"
|
||||
NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}"
|
||||
NYM_CI_WWW_LOCATION: "wallet-${{ env.GITHUB_REF_SLUG }}"
|
||||
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
|
||||
GIT_BRANCH: "${GITHUB_REF##*/}"
|
||||
IS_SUCCESS: "${{ job.status == 'success' }}"
|
||||
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
|
||||
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM }}"
|
||||
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
|
||||
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
|
||||
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
|
||||
uses: docker://keybaseio/client:stable-node
|
||||
with:
|
||||
args: .github/workflows/support-files/notifications/entry_point.sh
|
||||
@@ -19,7 +19,11 @@ jobs:
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-binaries-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
runs-on: arc-ubuntu-22.04
|
||||
matrix:
|
||||
include:
|
||||
- os: arc-ubuntu-22.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
outputs:
|
||||
release_id: ${{ steps.create-release.outputs.id }}
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
>
|
||||
> ➡️➡️➡️➡️➡️ **View output:**
|
||||
>
|
||||
> `storybook`: https://{{ env.NYM_CI_WWW_LOCATION }}.{{ env.NYM_CI_WWW_BASE }}
|
||||
>
|
||||
> `branch` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/tree/{{ env.GIT_BRANCH_NAME }}
|
||||
>
|
||||
> `commit` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/commit/{{ env.GITHUB_SHA }}
|
||||
|
||||
@@ -34,11 +34,12 @@ members = [
|
||||
"common/config",
|
||||
"common/cosmwasm-smart-contracts/coconut-dkg",
|
||||
"common/cosmwasm-smart-contracts/contracts-common",
|
||||
"common/cosmwasm-smart-contracts/contracts-common-testing",
|
||||
"common/cosmwasm-smart-contracts/easy_addr",
|
||||
"common/cosmwasm-smart-contracts/ecash-contract",
|
||||
"common/cosmwasm-smart-contracts/group-contract",
|
||||
"common/cosmwasm-smart-contracts/mixnet-contract",
|
||||
"common/cosmwasm-smart-contracts/multisig-contract",
|
||||
"common/cosmwasm-smart-contracts/multisig-contract", "common/cosmwasm-smart-contracts/nym-performance-contract",
|
||||
"common/cosmwasm-smart-contracts/nym-pool-contract",
|
||||
"common/cosmwasm-smart-contracts/vesting-contract",
|
||||
"common/credential-storage",
|
||||
@@ -126,6 +127,7 @@ members = [
|
||||
"service-providers/common",
|
||||
"service-providers/ip-packet-router",
|
||||
"service-providers/network-requester",
|
||||
"sqlx-pool-guard",
|
||||
"tools/echo-server",
|
||||
"tools/internal/contract-state-importer/importer-cli",
|
||||
"tools/internal/contract-state-importer/importer-contract",
|
||||
@@ -135,7 +137,6 @@ members = [
|
||||
"tools/internal/testnet-manager",
|
||||
"tools/internal/testnet-manager",
|
||||
"tools/internal/testnet-manager/dkg-bypass-contract",
|
||||
"tools/internal/testnet-manager/dkg-bypass-contract",
|
||||
"tools/internal/validator-status-check",
|
||||
"tools/nym-cli",
|
||||
"tools/nym-id-cli",
|
||||
@@ -287,6 +288,7 @@ petgraph = "0.6.5"
|
||||
pin-project = "1.1"
|
||||
pin-project-lite = "0.2.16"
|
||||
publicsuffix = "2.3.0"
|
||||
proc_pidinfo = "0.1.3"
|
||||
quote = "1"
|
||||
rand = "0.8.5"
|
||||
rand_chacha = "0.3"
|
||||
@@ -368,9 +370,6 @@ subtle = "2.5.0"
|
||||
# cosmwasm-related
|
||||
cosmwasm-schema = "=2.2.2"
|
||||
cosmwasm-std = "=2.2.2"
|
||||
# use 1.0.1 as that's the version used by cosmwasm-std 2.2.1
|
||||
# (and ideally we don't want to pull the same dependency twice)
|
||||
serde-json-wasm = "=1.0.1"
|
||||
# same version as used by cosmwasm
|
||||
cw-utils = "=2.0.0"
|
||||
cw-storage-plus = "=2.0.0"
|
||||
@@ -378,6 +377,7 @@ cw2 = { version = "=2.0.0" }
|
||||
cw3 = { version = "=2.0.0" }
|
||||
cw4 = { version = "=2.0.0" }
|
||||
cw-controllers = { version = "=2.0.0" }
|
||||
cw-multi-test = "=2.3.2"
|
||||
|
||||
# cosmrs-related
|
||||
bip32 = { version = "0.5.3", default-features = false }
|
||||
|
||||
@@ -133,7 +133,7 @@ clippy: sdk-wasm-lint
|
||||
# Build contracts ready for deploy
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
CONTRACTS=vesting_contract mixnet_contract nym_ecash cw3_flex_multisig cw4_group nym_coconut_dkg nym_pool_contract
|
||||
CONTRACTS=vesting_contract mixnet_contract nym_ecash cw3_flex_multisig cw4_group nym_coconut_dkg nym_pool_contract nym_performance_contract
|
||||
CONTRACTS_WASM=$(addsuffix .wasm, $(CONTRACTS))
|
||||
CONTRACTS_OUT_DIR=contracts/target/wasm32-unknown-unknown/release
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Shared assets
|
||||
|
||||
This directory contains asset files shared by many projects in this repo.
|
||||
|
||||
You will find:
|
||||
|
||||
- favicons
|
||||
- logos
|
||||
- shared fonts
|
||||
- shared icon SVGs
|
||||
|
||||
See [ts-packages/react-webpack-with-theme-example](../ts-packages/react-webpack-with-theme-example) for examples of usage.
|
||||
|
After Width: | Height: | Size: 545 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
@@ -0,0 +1,10 @@
|
||||
<svg width="64" height="64" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M40 78.5C61.263 78.5 78.5 61.263 78.5 40C78.5 18.737 61.263 1.5 40 1.5C18.737 1.5 1.5 18.737 1.5 40C1.5 61.263 18.737 78.5 40 78.5Z" fill="#070B15" stroke="url(#paint0_linear_0_1)" stroke-width="3"/>
|
||||
<path d="M31.4894 27.56L41.8623 56H48.5106H56V24H48.5106V52.4L38.1777 24H31.4894H24V56H31.4894V27.56Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_0_1" x1="0.839161" y1="80" x2="80" y2="80" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.09375" stop-color="#FB6E4E"/>
|
||||
<stop offset="1" stop-color="#F51473"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 672 B |
@@ -0,0 +1,6 @@
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
src: url('./OpenSans-VariableFont_wdth,wght.ttf') format('truetype-variations'),
|
||||
url('./OpenSans-Italic-VariableFont_wdth,wght.ttf') format('truetype-variations');
|
||||
font-weight: 100 1000;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
Update fonts by doing the following:
|
||||
|
||||
1. Go to https://fonts.google.com/specimen/Open+Sans
|
||||
2. Add all the styles you want and select `@import`
|
||||
3. Copy the url (e.g. curl https://fonts.googleapis.com/css2\?family\=Open+Sans:ital,wght@0,300\;0,400\;0,500\;0,600\;0,700\;0,800\;1,300\;1,400\;1,500\;1,600\;1,700\;1,800\&display\=swap)
|
||||
4. Run `curl curl https://fonts.googleapis.com/css2\?family\=Open+Sans:ital,wght@0,300\;0,400\;0,500\;0,600\;0,700\;0,800\;1,300\;1,400\;1,500\;1,600\;1,700\;1,800\&display\=swap`
|
||||
5. Use the response as the CSS import directives and download the font files for each font weight
|
||||
6. Remember to delete any old font files
|
||||
@@ -0,0 +1,96 @@
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
font-stretch: normal;
|
||||
font-display: swap;
|
||||
src: url(./memQYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWq8tWZ0Pw86hd0Rk5hkaVc.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-stretch: normal;
|
||||
font-display: swap;
|
||||
src: url(./memQYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWq8tWZ0Pw86hd0Rk8ZkaVc.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
font-stretch: normal;
|
||||
font-display: swap;
|
||||
src: url(./memQYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWq8tWZ0Pw86hd0Rk_RkaVc.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
font-stretch: normal;
|
||||
font-display: swap;
|
||||
src: url(./memQYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWq8tWZ0Pw86hd0RkxhjaVc.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-stretch: normal;
|
||||
font-display: swap;
|
||||
src: url(./memQYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWq8tWZ0Pw86hd0RkyFjaVc.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
font-stretch: normal;
|
||||
font-display: swap;
|
||||
src: url(./memQYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWq8tWZ0Pw86hd0Rk0ZjaVc.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-stretch: normal;
|
||||
font-display: swap;
|
||||
src: url(./memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsiH0C4n.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: normal;
|
||||
font-display: swap;
|
||||
src: url(./memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0C4n.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-stretch: normal;
|
||||
font-display: swap;
|
||||
src: url(./memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjr0C4n.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-stretch: normal;
|
||||
font-display: swap;
|
||||
src: url(./memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsgH1y4n.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-stretch: normal;
|
||||
font-display: swap;
|
||||
src: url(./memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsg-1y4n.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
font-stretch: normal;
|
||||
font-display: swap;
|
||||
src: url(./memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgshZ1y4n.ttf) format('truetype');
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 80 80"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_421_13045)">
|
||||
<path
|
||||
d="M40 80C62.0914 80 80 62.0914 80 40C80 17.9086 62.0914 0 40 0C17.9086 0 0 17.9086 0 40C0 62.0914 17.9086 80 40 80Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M40 77.2636C60.5801 77.2636 77.2636 60.5801 77.2636 40C77.2636 19.4199 60.5801 2.73645 40 2.73645C19.4199 2.73645 2.73645 19.4199 2.73645 40C2.73645 60.5801 19.4199 77.2636 40 77.2636Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M24.0224 32.471H23.9776V32.5084V45.5775L18.4673 32.4934L18.4598 32.471H18.4299H15.3047H11.7981H11.7607V32.5084V47.4916V47.529H11.7981H15.3047H15.3421V47.4916V34.4L20.8748 47.5065L20.8822 47.529H20.9121H24.0224H27.5215H27.5589V47.4916V32.5084V32.471H27.5215H24.0224Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M23.8965 32.39H27.64V47.6101H20.8238L20.7989 47.5352L15.4232 34.8006V47.6101H11.6797V32.39H18.5183L18.5432 32.4649L23.8965 45.1761V32.39ZM23.9776 45.5776L18.4673 32.4935L18.4598 32.471H11.7608V47.529H15.3421V34.4L20.8748 47.5065L20.8823 47.529H27.5589V32.471H23.9776V45.5776Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M68.2019 32.471H61.5178H61.4804L61.4729 32.5009L58.0486 45.6374L54.6169 32.5009L54.6094 32.471H54.5795H47.8804H47.8355V32.5084V47.4916V47.529H47.8804H51.3795H51.4169V47.4916V34.5047L54.8038 47.499L54.8112 47.529H54.8486H61.2337H61.2636L61.2711 47.499L64.658 34.5047V47.4916V47.529H64.6954H68.2019H68.2393V47.4916V32.5084V32.471H68.2019Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M61.4171 32.39H68.3204V47.6101H64.5769V35.1372L61.3497 47.5187L61.3495 47.5195L61.3269 47.6101H54.7479L54.7253 47.5195L54.7251 47.5187L51.4979 35.1372V47.6101H47.7545V32.39H54.6727L54.6953 32.4804L54.6955 32.4813L58.0485 45.3163L61.3943 32.4813L61.3945 32.4805L61.4171 32.39ZM58.0486 45.6374L54.6168 32.5009L54.6094 32.471H47.8355V47.529H51.4168V34.5047L54.8038 47.4991L54.8112 47.529H61.2636L61.2711 47.4991L64.658 34.5047V47.529H68.2393V32.471H61.4804L61.4729 32.5009L58.0486 45.6374Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M42.0711 32.471H42.0486L42.0412 32.486L37.7869 39.8804L33.5103 32.486L33.5028 32.471H33.4804H29.4355H29.3608L29.3982 32.5308L35.9851 43.9402V47.4916V47.529H36.0225H39.529H39.5664V47.4916V43.9402L46.1533 32.5308L46.1907 32.471H46.1159H42.0711Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M41.9985 32.39H46.337L46.2228 32.5726L39.6475 43.9619V47.6101H35.904V43.9619L29.3286 32.5726L29.2145 32.39H33.5529L33.5817 32.4475L37.7868 39.7181L41.9697 32.4476L41.9985 32.39ZM42.0411 32.486L37.7869 39.8804L33.5103 32.486L33.5028 32.471H29.3608L29.3981 32.5308L35.9851 43.9402V47.529H39.5664V43.9402L46.1533 32.5308L46.1907 32.471H42.0486L42.0411 32.486Z"
|
||||
fill="black"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_421_13045">
|
||||
<rect width="80" height="80" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,10 @@
|
||||
<svg width="64" height="64" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M40 78.5C61.263 78.5 78.5 61.263 78.5 40C78.5 18.737 61.263 1.5 40 1.5C18.737 1.5 1.5 18.737 1.5 40C1.5 61.263 18.737 78.5 40 78.5Z" fill="#070B15" stroke="url(#paint0_linear_0_1)" stroke-width="3"/>
|
||||
<path d="M31.4894 27.56L41.8623 56H48.5106H56V24H48.5106V52.4L38.1777 24H31.4894H24V56H31.4894V27.56Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_0_1" x1="0.839161" y1="80" x2="80" y2="80" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.09375" stop-color="#FB6E4E"/>
|
||||
<stop offset="1" stop-color="#F51473"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 714 B |
@@ -0,0 +1,13 @@
|
||||
<svg width="300" height="300" viewBox="0 0 296 296" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M148 296C229.738 296 296 229.738 296 148C296 66.2619 229.738 0 148 0C66.2619 0 0 66.2619 0 148C0 229.738 66.2619 296 148 296Z" fill="url(#paint0_linear_113_1244)"/>
|
||||
<path d="M148 285.875C224.147 285.875 285.875 224.146 285.875 148C285.875 71.8536 224.147 10.1248 148 10.1248C71.8538 10.1248 10.125 71.8536 10.125 148C10.125 224.146 71.8538 285.875 148 285.875Z" fill="#121725"/>
|
||||
<path d="M88.8829 120.143H88.7169V120.281V168.637L68.3289 120.226L68.3012 120.143H68.1905H56.6272H43.653H43.5146V120.281V175.719V175.857H43.653H56.6272H56.7655V175.719V127.28L77.2365 175.774L77.2642 175.857H77.3748H88.8829H101.829H101.968V175.719V120.281V120.143H101.829H88.8829Z" fill="white"/>
|
||||
<path d="M252.347 120.143H227.616H227.477L227.45 120.253L214.78 168.858L202.082 120.253L202.054 120.143H201.944H177.157H176.991V120.281V175.719V175.857H177.157H190.104H190.242V175.719V127.667L202.774 175.747L202.801 175.857H202.94H226.564H226.675L226.703 175.747L239.234 127.667V175.719V175.857H239.373H252.347H252.485V175.719V120.281V120.143H252.347Z" fill="white"/>
|
||||
<path d="M155.663 120.143H155.58L155.552 120.198L139.812 147.557L123.988 120.198L123.96 120.143H123.877H108.911H108.635L108.773 120.364L133.145 162.579V175.719V175.857H133.283H146.257H146.396V175.719V162.579L170.767 120.364L170.905 120.143H170.629H155.663Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_113_1244" x1="0" y1="148" x2="296" y2="148" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.09375" stop-color="#FB6E4E"/>
|
||||
<stop offset="1" stop-color="#FC1D60"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg viewBox="0 0 210 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M45.8829 0.142822H45.7169V0.28114V48.637L25.3289 0.225818L25.3012 0.142822H25.1905H13.6272H0.652966H0.514648V0.28114V55.7189V55.8572H0.652966H13.6272H13.7655V55.7189V7.28002L34.2365 55.7742L34.2642 55.8572H34.3748H45.8829H58.8294H58.9677V55.7189V0.28114V0.142822H58.8294H45.8829Z"/>
|
||||
<path d="M209.347 0.142822H184.616H184.477L184.45 0.253483L171.78 48.8583L159.082 0.253483L159.054 0.142822H158.944H134.157H133.991V0.28114V55.7189V55.8572H134.157H147.104H147.242V55.7189V7.66731L159.774 55.7466L159.801 55.8572H159.94H183.564H183.675L183.703 55.7466L196.234 7.66731V55.7189V55.8572H196.373H209.347H209.485V55.7189V0.28114V0.142822H209.347Z"/>
|
||||
<path d="M112.663 0.142822H112.58L112.552 0.198153L96.8116 27.5574L80.988 0.198153L80.9604 0.142822H80.8774H65.9114H65.6348L65.7731 0.364136L90.1447 42.5787V55.7189V55.8572H90.283H103.257H103.396V55.7189V42.5787L127.767 0.364136L127.905 0.142822H127.629H112.663Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1011 B |
@@ -0,0 +1,7 @@
|
||||
<svg viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M171.7,30.3001 C132.7,-8.7999 69.3001,-8.7999 30.3001,30.3001 C-8.7999,69.4001 -8.7999,132.7 30.3001,171.7 C69.4001,210.8 132.7,210.8 171.7,171.7 C210.8,132.7 210.8,69.3001 171.7,30.3001 Z M163.1,163.1 C128.8,197.4 73.1001,197.4 38.8001,163.1 C4.5001,128.8 4.5001,73.1001 38.8001,38.8001 C73.1001,4.5001 128.8,4.5001 163.1,38.8001 C197.5,73.2001 197.5,128.8 163.1,163.1 Z" id="Shape" fill="#fff"></path>
|
||||
<path d="M163.1,38.9 C128.8,4.60005 73.1002,4.60005 38.8002,38.9 C4.50019,73.2 4.50019,128.9 38.8002,163.2 C73.1002,197.5 128.8,197.5 163.1,163.2 C197.5,128.8 197.5,73.2 163.1,38.9 Z" id="Shape" fill="#000"></path>
|
||||
<g id="T" transform="translate(25, 25) scale(5,5)">
|
||||
<path d="M18.4804688,24 C19.203125,24 19.7182617,23.8608398 20.0258789,23.5825195 C20.3334961,23.3041992 20.4873047,22.9453125 20.4873047,22.5058594 C20.4873047,22.0566406 20.3334961,21.6928711 20.0258789,21.4145508 C19.7182617,21.1362305 19.203125,20.9970703 18.4804688,20.9970703 L18.4804688,20.9970703 L16.4589844,20.9970703 L16.4589844,9.24902344 L19.7548828,9.24902344 L19.7548828,12.0908203 C19.7548828,12.8134766 19.894043,13.3286133 20.1723633,13.6362305 C20.4506836,13.9438477 20.8095703,14.0976562 21.2490234,14.0976562 C21.6982422,14.0976562 22.0620117,13.9438477 22.340332,13.6362305 C22.6186523,13.3286133 22.7578125,12.8134766 22.7578125,12.0908203 L22.7578125,12.0908203 L22.7578125,6.24609375 L7.20117188,6.23144531 L7.20117188,12.0908203 C7.20117188,12.8134766 7.34033203,13.3286133 7.61865234,13.6362305 C7.89697266,13.9438477 8.25585938,14.0976562 8.6953125,14.0976562 C9.14453125,14.0976562 9.50830078,13.9438477 9.78662109,13.6362305 C10.0649414,13.3286133 10.2041016,12.8134766 10.2041016,12.0908203 L10.2041016,12.0908203 L10.2041016,9.24902344 L13.4560547,9.24902344 L13.4560547,20.9970703 L11.4492188,20.9970703 C10.7265625,20.9970703 10.2114258,21.1362305 9.90380859,21.4145508 C9.59619141,21.6928711 9.44238281,22.0517578 9.44238281,22.4912109 C9.44238281,22.9404297 9.59619141,23.3041992 9.90380859,23.5825195 C10.2114258,23.8608398 10.7265625,24 11.4492188,24 L11.4492188,24 L18.4804688,24 Z" id="T" fill="#fff"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M170.7 29.3001C131.7 -9.7999 68.3001 -9.7999 29.3001 29.3001C-9.7999 68.4001 -9.7999 131.7 29.3001 170.7C68.4001 209.8 131.7 209.8 170.7 170.7C209.8 131.7 209.8 68.3001 170.7 29.3001ZM162.1 162.1C127.8 196.4 72.1001 196.4 37.8001 162.1C3.5001 127.8 3.5001 72.1001 37.8001 37.8001C72.1001 3.5001 127.8 3.5001 162.1 37.8001C196.5 72.2001 196.5 127.8 162.1 162.1Z" fill="white"/>
|
||||
<path d="M162.1 37.9C127.8 3.60005 72.1002 3.60005 37.8002 37.9C3.50019 72.2 3.50019 127.9 37.8002 162.2C72.1002 196.5 127.8 196.5 162.1 162.2C196.5 127.8 196.5 72.2 162.1 37.9ZM63.0002 170.7C56.8002 167.4 51.1002 163.2 46.1002 158.4V41.7C51.3002 36.7 57.2002 32.5 63.6002 29.1L137 140.9V29.3C143.2 32.6 148.9 36.8 153.9 41.6V158.3C148.7 163.3 142.8 167.5 136.4 170.9L63.0002 59.1V170.7Z" fill="#070B15"/>
|
||||
<path d="M154 158.3V41.7C148.9 36.9 143.2 32.7 137.1 29.4V140.9L63.5 29C57.1 32.4 51.2 36.6 46 41.6V158.3C51.1 163.1 56.8 167.3 62.9 170.6V59.1L136.5 171C142.9 167.6 148.8 163.3 154 158.3Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,7 @@
|
||||
<svg viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M171.7,30.3001 C132.7,-8.7999 69.3001,-8.7999 30.3001,30.3001 C-8.7999,69.4001 -8.7999,132.7 30.3001,171.7 C69.4001,210.8 132.7,210.8 171.7,171.7 C210.8,132.7 210.8,69.3001 171.7,30.3001 Z M163.1,163.1 C128.8,197.4 73.1001,197.4 38.8001,163.1 C4.5001,128.8 4.5001,73.1001 38.8001,38.8001 C73.1001,4.5001 128.8,4.5001 163.1,38.8001 C197.5,73.2001 197.5,128.8 163.1,163.1 Z" id="Shape" fill="#141521"></path>
|
||||
<path d="M163.1,38.9 C128.8,4.60005 73.1002,4.60005 38.8002,38.9 C4.50019,73.2 4.50019,128.9 38.8002,163.2 C73.1002,197.5 128.8,197.5 163.1,163.2 C197.5,128.8 197.5,73.2 163.1,38.9 Z" id="Shape" fill="#FFFFFF"></path>
|
||||
<g id="T" transform="translate(25, 25) scale(5,5)">
|
||||
<path d="M18.4804688,24 C19.203125,24 19.7182617,23.8608398 20.0258789,23.5825195 C20.3334961,23.3041992 20.4873047,22.9453125 20.4873047,22.5058594 C20.4873047,22.0566406 20.3334961,21.6928711 20.0258789,21.4145508 C19.7182617,21.1362305 19.203125,20.9970703 18.4804688,20.9970703 L18.4804688,20.9970703 L16.4589844,20.9970703 L16.4589844,9.24902344 L19.7548828,9.24902344 L19.7548828,12.0908203 C19.7548828,12.8134766 19.894043,13.3286133 20.1723633,13.6362305 C20.4506836,13.9438477 20.8095703,14.0976562 21.2490234,14.0976562 C21.6982422,14.0976562 22.0620117,13.9438477 22.340332,13.6362305 C22.6186523,13.3286133 22.7578125,12.8134766 22.7578125,12.0908203 L22.7578125,12.0908203 L22.7578125,6.24609375 L7.20117188,6.23144531 L7.20117188,12.0908203 C7.20117188,12.8134766 7.34033203,13.3286133 7.61865234,13.6362305 C7.89697266,13.9438477 8.25585938,14.0976562 8.6953125,14.0976562 C9.14453125,14.0976562 9.50830078,13.9438477 9.78662109,13.6362305 C10.0649414,13.3286133 10.2041016,12.8134766 10.2041016,12.0908203 L10.2041016,12.0908203 L10.2041016,9.24902344 L13.4560547,9.24902344 L13.4560547,20.9970703 L11.4492188,20.9970703 C10.7265625,20.9970703 10.2114258,21.1362305 9.90380859,21.4145508 C9.59619141,21.6928711 9.44238281,22.0517578 9.44238281,22.4912109 C9.44238281,22.9404297 9.59619141,23.3041992 9.90380859,23.5825195 C10.2114258,23.8608398 10.7265625,24 11.4492188,24 L11.4492188,24 L18.4804688,24 Z" id="T" fill="#000" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M170.7 29.3001C131.7 -9.7999 68.3001 -9.7999 29.3001 29.3001C-9.7999 68.4001 -9.7999 131.7 29.3001 170.7C68.4001 209.8 131.7 209.8 170.7 170.7C209.8 131.7 209.8 68.3001 170.7 29.3001ZM162.1 162.1C127.8 196.4 72.1001 196.4 37.8001 162.1C3.5001 127.8 3.5001 72.1001 37.8001 37.8001C72.1001 3.5001 127.8 3.5001 162.1 37.8001C196.5 72.2001 196.5 127.8 162.1 162.1Z" fill="#141521"/>
|
||||
<path d="M162.1 37.9C127.8 3.60005 72.1002 3.60005 37.8002 37.9C3.50019 72.2 3.50019 127.9 37.8002 162.2C72.1002 196.5 127.8 196.5 162.1 162.2C196.5 127.8 196.5 72.2 162.1 37.9ZM63.0002 170.7C56.8002 167.4 51.1002 163.2 46.1002 158.4V41.7C51.3002 36.7 57.2002 32.5 63.6002 29.1L137 140.9V29.3C143.2 32.6 148.9 36.8 153.9 41.6V158.3C148.7 163.3 142.8 167.5 136.4 170.9L63.0002 59.1V170.7Z" fill="white"/>
|
||||
<path d="M154 158.3V41.7C148.9 36.9 143.2 32.7 137.1 29.4V140.9L63.5 29C57.1 32.4 51.2 36.6 46 41.6V158.3C51.1 163.1 56.8 167.3 62.9 170.6V59.1L136.5 171C142.9 167.6 148.8 163.3 154 158.3Z" fill="#141521"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -2,8 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::BadGateway;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::{io, path::PathBuf};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -19,7 +18,6 @@ pub enum StorageError {
|
||||
|
||||
#[error("failed to perform sqlx migration: {source}")]
|
||||
MigrationError {
|
||||
#[source]
|
||||
#[from]
|
||||
source: sqlx::migrate::MigrateError,
|
||||
},
|
||||
@@ -32,7 +30,6 @@ pub enum StorageError {
|
||||
|
||||
#[error("failed to run the SQL query: {source}")]
|
||||
QueryError {
|
||||
#[source]
|
||||
#[from]
|
||||
source: sqlx::error::Error,
|
||||
},
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::client::replies::reply_storage::{
|
||||
fs_backend, CombinedReplyStorage, ReplyStorageBackend,
|
||||
use crate::{
|
||||
client::replies::reply_storage::{fs_backend, CombinedReplyStorage, ReplyStorageBackend},
|
||||
config,
|
||||
config::Config,
|
||||
error::ClientCoreError,
|
||||
};
|
||||
use crate::config;
|
||||
use crate::config::Config;
|
||||
use crate::error::ClientCoreError;
|
||||
use log::{error, info, trace};
|
||||
use nym_bandwidth_controller::BandwidthController;
|
||||
use nym_client_core_gateways_storage::OnDiskGatewaysDetails;
|
||||
use nym_credential_storage::storage::Storage as CredentialStorage;
|
||||
use nym_validator_client::nyxd;
|
||||
use nym_validator_client::QueryHttpRpcNyxdClient;
|
||||
use std::path::Path;
|
||||
use std::{fs, io};
|
||||
use nym_validator_client::{nyxd, QueryHttpRpcNyxdClient};
|
||||
use std::{io, path::Path};
|
||||
use time::OffsetDateTime;
|
||||
use url::Url;
|
||||
|
||||
@@ -22,11 +20,11 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
|
||||
db_path: P,
|
||||
surb_config: &config::ReplySurbs,
|
||||
) -> Result<fs_backend::Backend, ClientCoreError> {
|
||||
info!("creating fresh surb database");
|
||||
info!("Creating fresh surb database");
|
||||
let mut storage_backend = match fs_backend::Backend::init(db_path).await {
|
||||
Ok(backend) => backend,
|
||||
Err(err) => {
|
||||
error!("failed to setup persistent storage backend for our reply needs: {err}");
|
||||
error!("setup_fresh_backend: Failed to setup persistent storage backend for our reply needs: {err}");
|
||||
return Err(ClientCoreError::SurbStorageError {
|
||||
source: Box::new(err),
|
||||
});
|
||||
@@ -40,14 +38,15 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
|
||||
surb_config.minimum_reply_surb_storage_threshold,
|
||||
surb_config.maximum_reply_surb_storage_threshold,
|
||||
);
|
||||
storage_backend
|
||||
.init_fresh(&mem_store)
|
||||
.await
|
||||
.map_err(|err| ClientCoreError::SurbStorageError {
|
||||
source: Box::new(err),
|
||||
})?;
|
||||
|
||||
Ok(storage_backend)
|
||||
match storage_backend.init_fresh(&mem_store).await {
|
||||
Ok(()) => Ok(storage_backend),
|
||||
Err(err) => {
|
||||
storage_backend.shutdown().await;
|
||||
Err(ClientCoreError::SurbStorageError {
|
||||
source: Box::new(err),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fn setup_inactive_backend(surb_config: &config::ReplySurbs) -> fs_backend::Backend {
|
||||
@@ -58,12 +57,11 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
|
||||
// )
|
||||
// }
|
||||
|
||||
fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()> {
|
||||
async fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()> {
|
||||
let db_path = db_path.as_ref();
|
||||
debug_assert!(db_path.exists());
|
||||
|
||||
let now = OffsetDateTime::now_utc().unix_timestamp();
|
||||
|
||||
let suffix = format!("_{now}.corrupted");
|
||||
|
||||
let new_extension =
|
||||
@@ -72,11 +70,15 @@ fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()> {
|
||||
} else {
|
||||
suffix
|
||||
};
|
||||
let renamed = db_path.with_extension(new_extension);
|
||||
|
||||
let mut renamed = db_path.to_owned();
|
||||
renamed.set_extension(new_extension);
|
||||
|
||||
fs::rename(db_path, renamed)
|
||||
tokio::fs::rename(db_path, &renamed).await.inspect_err(|_| {
|
||||
error!(
|
||||
"Failed to rename corrupt database file: {} to {}",
|
||||
db_path.display(),
|
||||
renamed.display()
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
|
||||
@@ -87,13 +89,12 @@ pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
|
||||
// the existing one
|
||||
let db_path = db_path.as_ref();
|
||||
if db_path.exists() {
|
||||
info!("loading existing surb database");
|
||||
info!("Loading existing surb database");
|
||||
match fs_backend::Backend::try_load(db_path, surb_config.fresh_sender_tags).await {
|
||||
Ok(backend) => Ok(backend),
|
||||
Err(err) => {
|
||||
error!("failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future");
|
||||
|
||||
archive_corrupted_database(db_path)?;
|
||||
error!("setup_fs_reply_surb_backend: Failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future");
|
||||
archive_corrupted_database(db_path).await?;
|
||||
setup_fresh_backend(db_path, surb_config).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,15 +17,26 @@ nym-crypto = { path = "../../crypto", optional = true, default-features = false
|
||||
nym-sphinx = { path = "../../nymsphinx" }
|
||||
nym-task = { path = "../../task" }
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio]
|
||||
workspace = true
|
||||
features = ["fs"]
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
|
||||
workspace = true
|
||||
features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]
|
||||
optional = true
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx-pool-guard]
|
||||
path = "../../../sqlx-pool-guard"
|
||||
|
||||
[build-dependencies]
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
|
||||
sqlx = { workspace = true, features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
"macros",
|
||||
"migrate",
|
||||
] }
|
||||
|
||||
[features]
|
||||
fs-surb-storage = ["sqlx", "nym-crypto", "nym-crypto/hashing"]
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::{io, path::PathBuf};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -30,7 +29,6 @@ pub enum StorageError {
|
||||
|
||||
#[error("failed to perform sqlx migration: {source}")]
|
||||
MigrationError {
|
||||
#[source]
|
||||
#[from]
|
||||
source: sqlx::migrate::MigrateError,
|
||||
},
|
||||
@@ -43,7 +41,6 @@ pub enum StorageError {
|
||||
|
||||
#[error("failed to run the SQL query: {source}")]
|
||||
QueryError {
|
||||
#[source]
|
||||
#[from]
|
||||
source: sqlx::error::Error,
|
||||
},
|
||||
|
||||
@@ -15,9 +15,11 @@ use sqlx::{
|
||||
};
|
||||
use std::path::Path;
|
||||
|
||||
use sqlx_pool_guard::SqlitePoolGuard;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StorageManager {
|
||||
pub connection_pool: sqlx::SqlitePool,
|
||||
connection_pool: SqlitePoolGuard,
|
||||
}
|
||||
|
||||
// all SQL goes here
|
||||
@@ -37,7 +39,7 @@ impl StorageManager {
|
||||
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
|
||||
.synchronous(SqliteSynchronous::Normal)
|
||||
.auto_vacuum(SqliteAutoVacuum::Incremental)
|
||||
.filename(database_path)
|
||||
.filename(&database_path)
|
||||
.create_if_missing(fresh)
|
||||
.disable_statement_logging();
|
||||
|
||||
@@ -49,11 +51,15 @@ impl StorageManager {
|
||||
}
|
||||
};
|
||||
|
||||
let connection_pool =
|
||||
SqlitePoolGuard::new(database_path.as_ref().to_path_buf(), connection_pool);
|
||||
|
||||
if let Err(err) = sqlx::migrate!("./fs_surbs_migrations")
|
||||
.run(&connection_pool)
|
||||
.run(&*connection_pool)
|
||||
.await
|
||||
{
|
||||
error!("Failed to initialize SQLx database: {err}");
|
||||
connection_pool.close().await;
|
||||
return Err(err.into());
|
||||
}
|
||||
|
||||
@@ -61,38 +67,43 @@ impl StorageManager {
|
||||
Ok(StorageManager { connection_pool })
|
||||
}
|
||||
|
||||
/// Close connection pool waiting for all connections to be closed.
|
||||
pub async fn close_pool(&self) {
|
||||
self.connection_pool.close().await;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn status_table_exists(&self) -> Result<bool, sqlx::Error> {
|
||||
sqlx::query!("SELECT name FROM sqlite_master WHERE type='table' AND name='status'")
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.fetch_optional(&*self.connection_pool)
|
||||
.await
|
||||
.map(|r| r.is_some())
|
||||
}
|
||||
|
||||
pub async fn create_status_table(&self) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("INSERT INTO status(flush_in_progress, previous_flush_timestamp, client_in_use) VALUES (0, 0, 1)")
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_flush_status(&self) -> Result<bool, sqlx::Error> {
|
||||
sqlx::query!("SELECT flush_in_progress FROM status;")
|
||||
.fetch_one(&self.connection_pool)
|
||||
.fetch_one(&*self.connection_pool)
|
||||
.await
|
||||
.map(|r| r.flush_in_progress > 0)
|
||||
}
|
||||
|
||||
pub async fn set_previous_flush_timestamp(&self, timestamp: i64) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("UPDATE status SET previous_flush_timestamp = ?", timestamp)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_previous_flush_timestamp(&self) -> Result<i64, sqlx::Error> {
|
||||
sqlx::query!("SELECT previous_flush_timestamp FROM status;")
|
||||
.fetch_one(&self.connection_pool)
|
||||
.fetch_one(&*self.connection_pool)
|
||||
.await
|
||||
.map(|r| r.previous_flush_timestamp)
|
||||
}
|
||||
@@ -100,14 +111,14 @@ impl StorageManager {
|
||||
pub async fn set_flush_status(&self, in_progress: bool) -> Result<(), sqlx::Error> {
|
||||
let in_progress_int = i64::from(in_progress);
|
||||
sqlx::query!("UPDATE status SET flush_in_progress = ?", in_progress_int)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_client_in_use_status(&self) -> Result<bool, sqlx::Error> {
|
||||
sqlx::query!("SELECT client_in_use FROM status;")
|
||||
.fetch_one(&self.connection_pool)
|
||||
.fetch_one(&*self.connection_pool)
|
||||
.await
|
||||
.map(|r| r.client_in_use > 0)
|
||||
}
|
||||
@@ -115,21 +126,21 @@ impl StorageManager {
|
||||
pub async fn set_client_in_use_status(&self, in_use: bool) -> Result<(), sqlx::Error> {
|
||||
let in_use_int = i64::from(in_use);
|
||||
sqlx::query!("UPDATE status SET client_in_use = ?", in_use_int)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_all_tags(&self) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("DELETE FROM sender_tag;")
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_tags(&self) -> Result<Vec<StoredSenderTag>, sqlx::Error> {
|
||||
sqlx::query_as!(StoredSenderTag, "SELECT * FROM sender_tag;",)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.fetch_all(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -141,21 +152,21 @@ impl StorageManager {
|
||||
stored_tag.recipient,
|
||||
stored_tag.tag
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_all_reply_keys(&self) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("DELETE FROM reply_key;")
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_reply_keys(&self) -> Result<Vec<StoredReplyKey>, sqlx::Error> {
|
||||
sqlx::query_as!(StoredReplyKey, "SELECT * FROM reply_key;",)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.fetch_all(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -171,14 +182,14 @@ impl StorageManager {
|
||||
stored_reply_key.reply_key,
|
||||
stored_reply_key.sent_at_timestamp
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_surb_senders(&self) -> Result<Vec<StoredSurbSender>, sqlx::Error> {
|
||||
sqlx::query_as!(StoredSurbSender, "SELECT * FROM reply_surb_sender;",)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.fetch_all(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -193,7 +204,7 @@ impl StorageManager {
|
||||
stored_surb_sender.tag,
|
||||
stored_surb_sender.last_sent_timestamp
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?
|
||||
.last_insert_rowid();
|
||||
Ok(id)
|
||||
@@ -211,17 +222,17 @@ impl StorageManager {
|
||||
"#,
|
||||
sender_id
|
||||
)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.fetch_all(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_all_reply_surb_data(&self) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("DELETE FROM reply_surb;")
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query!("DELETE FROM reply_surb_sender;")
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
@@ -239,7 +250,7 @@ impl StorageManager {
|
||||
stored_reply_surb.reply_surb,
|
||||
stored_reply_surb.encoded_key_rotation
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -253,7 +264,7 @@ impl StorageManager {
|
||||
SELECT min_reply_surb_threshold as "min_reply_surb_threshold: u32", max_reply_surb_threshold as "max_reply_surb_threshold: u32" FROM reply_surb_storage_metadata;
|
||||
"#,
|
||||
)
|
||||
.fetch_one(&self.connection_pool)
|
||||
.fetch_one(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -267,7 +278,7 @@ impl StorageManager {
|
||||
"#,
|
||||
metadata.min_reply_surb_threshold,
|
||||
metadata.max_reply_surb_threshold,
|
||||
).execute(&self.connection_pool).await?;
|
||||
).execute(&*self.connection_pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::backend::fs_backend::manager::StorageManager;
|
||||
use crate::backend::fs_backend::models::{
|
||||
ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag, StoredSurbSender,
|
||||
};
|
||||
use crate::surb_storage::ReceivedReplySurbs;
|
||||
use crate::{
|
||||
CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys, UsedSenderTags,
|
||||
backend::fs_backend::{
|
||||
manager::StorageManager,
|
||||
models::{
|
||||
ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag,
|
||||
StoredSurbSender,
|
||||
},
|
||||
},
|
||||
surb_storage::ReceivedReplySurbs,
|
||||
CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys,
|
||||
UsedSenderTags,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use log::{debug, error, info, warn};
|
||||
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
@@ -41,15 +44,17 @@ impl Backend {
|
||||
}
|
||||
|
||||
let manager = StorageManager::init(database_path, true).await?;
|
||||
manager.create_status_table().await?;
|
||||
|
||||
let backend = Backend {
|
||||
temporary_old_path: None,
|
||||
database_path: owned_path,
|
||||
manager,
|
||||
};
|
||||
|
||||
Ok(backend)
|
||||
match manager.create_status_table().await {
|
||||
Ok(()) => Ok(Backend {
|
||||
temporary_old_path: None,
|
||||
database_path: owned_path,
|
||||
manager,
|
||||
}),
|
||||
Err(err) => {
|
||||
manager.close_pool().await;
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn try_load<P: AsRef<Path>>(
|
||||
@@ -64,7 +69,28 @@ impl Backend {
|
||||
}
|
||||
|
||||
let manager = StorageManager::init(database_path, false).await?;
|
||||
match Self::try_load_inner(&manager, fresh_sender_tags).await {
|
||||
Ok(()) => Ok(Backend {
|
||||
temporary_old_path: None,
|
||||
database_path: owned_path,
|
||||
manager,
|
||||
}),
|
||||
Err(e) => {
|
||||
manager.close_pool().await;
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gracefully close sqlite connection pool and drop backend.
|
||||
pub async fn shutdown(self) {
|
||||
self.manager.close_pool().await
|
||||
}
|
||||
|
||||
async fn try_load_inner(
|
||||
manager: &StorageManager,
|
||||
fresh_sender_tags: bool,
|
||||
) -> Result<(), StorageError> {
|
||||
// the database flush wasn't fully finished and thus the data is in inconsistent state
|
||||
// (we don't really know what's properly saved or what's not)
|
||||
if manager.get_flush_status().await? {
|
||||
@@ -126,20 +152,11 @@ impl Backend {
|
||||
manager.delete_all_tags().await?;
|
||||
}
|
||||
|
||||
Ok(Backend {
|
||||
temporary_old_path: None,
|
||||
database_path: owned_path,
|
||||
// manager: StorageManagerState::Storage(manager),
|
||||
manager,
|
||||
})
|
||||
}
|
||||
|
||||
async fn close_pool(&mut self) {
|
||||
self.manager.connection_pool.close().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn rotate(&mut self) -> Result<(), StorageError> {
|
||||
self.close_pool().await;
|
||||
self.manager.close_pool().await;
|
||||
|
||||
let new_extension = if let Some(existing_extension) =
|
||||
self.database_path.extension().and_then(|ext| ext.to_str())
|
||||
@@ -152,7 +169,8 @@ impl Backend {
|
||||
let mut temp_old = self.database_path.clone();
|
||||
temp_old.set_extension(new_extension);
|
||||
|
||||
fs::rename(&self.database_path, &temp_old)
|
||||
tokio::fs::rename(&self.database_path, &temp_old)
|
||||
.await
|
||||
.map_err(|err| StorageError::DatabaseRenameError { source: err })?;
|
||||
self.manager = StorageManager::init(&self.database_path, true).await?;
|
||||
self.manager.create_status_table().await?;
|
||||
@@ -161,9 +179,10 @@ impl Backend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_old(&mut self) -> Result<(), StorageError> {
|
||||
async fn remove_old(&mut self) -> Result<(), StorageError> {
|
||||
if let Some(old_path) = self.temporary_old_path.take() {
|
||||
fs::remove_file(old_path)
|
||||
tokio::fs::remove_file(old_path)
|
||||
.await
|
||||
.map_err(|err| StorageError::DatabaseOldFileRemoveError { source: err })
|
||||
} else {
|
||||
warn!("the old database file doesn't seem to exist!");
|
||||
@@ -335,7 +354,7 @@ impl ReplyStorageBackend for Backend {
|
||||
self.dump_reply_surb_storage_metadata(surbs_ref).await?;
|
||||
self.dump_reply_surbs(surbs_ref).await?;
|
||||
|
||||
self.remove_old()?;
|
||||
self.remove_old().await?;
|
||||
self.end_storage_flush().await
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ where
|
||||
self.backend.load_surb_storage().await
|
||||
}
|
||||
|
||||
// this will have to get enabled after merging develop
|
||||
pub async fn flush_on_shutdown(
|
||||
mut self,
|
||||
mem_state: CombinedReplyStorage,
|
||||
@@ -50,7 +49,6 @@ where
|
||||
shutdown.recv().await;
|
||||
|
||||
info!("PersistentReplyStorage is flushing all reply-related data to underlying storage");
|
||||
info!("you MUST NOT forcefully shutdown now or you risk data corruption!");
|
||||
if let Err(err) = self.backend.flush_surb_storage(&mem_state).await {
|
||||
error!("failed to flush our reply-related data to the persistent storage: {err}")
|
||||
} else {
|
||||
|
||||
@@ -19,6 +19,7 @@ nym-vesting-contract-common = { path = "../../cosmwasm-smart-contracts/vesting-c
|
||||
nym-ecash-contract-common = { path = "../../cosmwasm-smart-contracts/ecash-contract" }
|
||||
nym-multisig-contract-common = { path = "../../cosmwasm-smart-contracts/multisig-contract" }
|
||||
nym-group-contract-common = { path = "../../cosmwasm-smart-contracts/group-contract" }
|
||||
nym-performance-contract-common = { path = "../../cosmwasm-smart-contracts/nym-performance-contract" }
|
||||
nym-serde-helpers = { path = "../../serde-helpers", features = ["hex", "base64"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -13,6 +13,7 @@ pub mod ecash_query_client;
|
||||
pub mod group_query_client;
|
||||
pub mod mixnet_query_client;
|
||||
pub mod multisig_query_client;
|
||||
pub mod performance_query_client;
|
||||
pub mod vesting_query_client;
|
||||
|
||||
// signing clients
|
||||
@@ -21,6 +22,7 @@ pub mod ecash_signing_client;
|
||||
pub mod group_signing_client;
|
||||
pub mod mixnet_signing_client;
|
||||
pub mod multisig_signing_client;
|
||||
pub mod performance_signing_client;
|
||||
pub mod vesting_signing_client;
|
||||
|
||||
// re-export query traits
|
||||
@@ -29,6 +31,7 @@ pub use ecash_query_client::{EcashQueryClient, PagedEcashQueryClient};
|
||||
pub use group_query_client::{GroupQueryClient, PagedGroupQueryClient};
|
||||
pub use mixnet_query_client::{MixnetQueryClient, PagedMixnetQueryClient};
|
||||
pub use multisig_query_client::{MultisigQueryClient, PagedMultisigQueryClient};
|
||||
pub use performance_query_client::{PagedPerformanceQueryClient, PerformanceQueryClient};
|
||||
pub use vesting_query_client::{PagedVestingQueryClient, VestingQueryClient};
|
||||
|
||||
// re-export signing traits
|
||||
@@ -37,6 +40,7 @@ pub use ecash_signing_client::EcashSigningClient;
|
||||
pub use group_signing_client::GroupSigningClient;
|
||||
pub use mixnet_signing_client::MixnetSigningClient;
|
||||
pub use multisig_signing_client::MultisigSigningClient;
|
||||
pub use performance_signing_client::PerformanceSigningClient;
|
||||
pub use vesting_signing_client::VestingSigningClient;
|
||||
|
||||
// helper for providing blanket implementation for query clients
|
||||
@@ -44,6 +48,7 @@ pub trait NymContractsProvider {
|
||||
// main
|
||||
fn mixnet_contract_address(&self) -> Option<&AccountId>;
|
||||
fn vesting_contract_address(&self) -> Option<&AccountId>;
|
||||
fn performance_contract_address(&self) -> Option<&AccountId>;
|
||||
|
||||
// coconut-related
|
||||
fn ecash_contract_address(&self) -> Option<&AccountId>;
|
||||
@@ -56,6 +61,7 @@ pub trait NymContractsProvider {
|
||||
pub struct TypedNymContracts {
|
||||
pub mixnet_contract_address: Option<AccountId>,
|
||||
pub vesting_contract_address: Option<AccountId>,
|
||||
pub performance_contract_address: Option<AccountId>,
|
||||
|
||||
pub ecash_contract_address: Option<AccountId>,
|
||||
pub group_contract_address: Option<AccountId>,
|
||||
@@ -76,6 +82,10 @@ impl TryFrom<NymContracts> for TypedNymContracts {
|
||||
.vesting_contract_address
|
||||
.map(|addr| addr.parse())
|
||||
.transpose()?,
|
||||
performance_contract_address: value
|
||||
.performance_contract_address
|
||||
.map(|addr| addr.parse())
|
||||
.transpose()?,
|
||||
ecash_contract_address: value
|
||||
.ecash_contract_address
|
||||
.map(|addr| addr.parse())
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::collect_paged;
|
||||
use crate::nyxd::contract_traits::NymContractsProvider;
|
||||
use crate::nyxd::error::NyxdError;
|
||||
use crate::nyxd::CosmWasmClient;
|
||||
use async_trait::async_trait;
|
||||
use cosmrs::AccountId;
|
||||
pub use nym_performance_contract_common::{
|
||||
msg::QueryMsg as PerformanceQueryMsg, types::NetworkMonitorResponse,
|
||||
};
|
||||
use nym_performance_contract_common::{
|
||||
EpochId, EpochMeasurementsPagedResponse, EpochNodePerformance, EpochPerformancePagedResponse,
|
||||
FullHistoricalPerformancePagedResponse, HistoricalPerformance, NetworkMonitorInformation,
|
||||
NetworkMonitorsPagedResponse, NodeId, NodeMeasurement, NodeMeasurementsResponse,
|
||||
NodePerformance, NodePerformancePagedResponse, NodePerformanceResponse, RetiredNetworkMonitor,
|
||||
RetiredNetworkMonitorsPagedResponse,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait PerformanceQueryClient {
|
||||
async fn query_performance_contract<T>(
|
||||
&self,
|
||||
query: PerformanceQueryMsg,
|
||||
) -> Result<T, NyxdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>;
|
||||
|
||||
async fn admin(&self) -> Result<cw_controllers::AdminResponse, NyxdError> {
|
||||
self.query_performance_contract(PerformanceQueryMsg::Admin {})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_node_performance(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
node_id: NodeId,
|
||||
) -> Result<NodePerformanceResponse, NyxdError> {
|
||||
self.query_performance_contract(PerformanceQueryMsg::NodePerformance { epoch_id, node_id })
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_node_performance_paged(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
start_after: Option<EpochId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<NodePerformancePagedResponse, NyxdError> {
|
||||
self.query_performance_contract(PerformanceQueryMsg::NodePerformancePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_node_measurements(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
node_id: NodeId,
|
||||
) -> Result<NodeMeasurementsResponse, NyxdError> {
|
||||
self.query_performance_contract(PerformanceQueryMsg::NodeMeasurements { epoch_id, node_id })
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_epoch_measurements_paged(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<EpochMeasurementsPagedResponse, NyxdError> {
|
||||
self.query_performance_contract(PerformanceQueryMsg::EpochMeasurementsPaged {
|
||||
epoch_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_epoch_performance_paged(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<EpochPerformancePagedResponse, NyxdError> {
|
||||
self.query_performance_contract(PerformanceQueryMsg::EpochPerformancePaged {
|
||||
epoch_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_full_historical_performance_paged(
|
||||
&self,
|
||||
start_after: Option<(EpochId, NodeId)>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<FullHistoricalPerformancePagedResponse, NyxdError> {
|
||||
self.query_performance_contract(PerformanceQueryMsg::FullHistoricalPerformancePaged {
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_network_monitor(
|
||||
&self,
|
||||
address: &AccountId,
|
||||
) -> Result<NetworkMonitorResponse, NyxdError> {
|
||||
self.query_performance_contract(PerformanceQueryMsg::NetworkMonitor {
|
||||
address: address.to_string(),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_network_monitors_paged(
|
||||
&self,
|
||||
start_after: Option<String>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<NetworkMonitorsPagedResponse, NyxdError> {
|
||||
self.query_performance_contract(PerformanceQueryMsg::NetworkMonitorsPaged {
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_retired_network_monitors_paged(
|
||||
&self,
|
||||
start_after: Option<String>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<RetiredNetworkMonitorsPagedResponse, NyxdError> {
|
||||
self.query_performance_contract(PerformanceQueryMsg::RetiredNetworkMonitorsPaged {
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
// extension trait to the query client to deal with the paged queries
|
||||
// (it didn't feel appropriate to combine it with the existing trait
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait PagedPerformanceQueryClient: PerformanceQueryClient {
|
||||
async fn get_all_node_performance(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Result<Vec<EpochNodePerformance>, NyxdError> {
|
||||
collect_paged!(self, get_node_performance_paged, performance, node_id)
|
||||
}
|
||||
|
||||
async fn get_all_epoch_measurements(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Result<Vec<NodeMeasurement>, NyxdError> {
|
||||
collect_paged!(self, get_epoch_measurements_paged, measurements, node_id)
|
||||
}
|
||||
|
||||
async fn get_all_epoch_performance(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<Vec<NodePerformance>, NyxdError> {
|
||||
collect_paged!(self, get_epoch_performance_paged, performance, epoch_id)
|
||||
}
|
||||
|
||||
async fn get_all_full_historical_performance(
|
||||
&self,
|
||||
) -> Result<Vec<HistoricalPerformance>, NyxdError> {
|
||||
collect_paged!(self, get_full_historical_performance_paged, performance)
|
||||
}
|
||||
|
||||
async fn get_all_network_monitors(&self) -> Result<Vec<NetworkMonitorInformation>, NyxdError> {
|
||||
collect_paged!(self, get_network_monitors_paged, info)
|
||||
}
|
||||
|
||||
async fn get_all_retired_network_monitors(
|
||||
&self,
|
||||
) -> Result<Vec<RetiredNetworkMonitor>, NyxdError> {
|
||||
collect_paged!(self, get_retired_network_monitors_paged, info)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T> PagedPerformanceQueryClient for T where T: PerformanceQueryClient {}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl<C> PerformanceQueryClient for C
|
||||
where
|
||||
C: CosmWasmClient + NymContractsProvider + Send + Sync,
|
||||
{
|
||||
async fn query_performance_contract<T>(
|
||||
&self,
|
||||
query: PerformanceQueryMsg,
|
||||
) -> Result<T, NyxdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>,
|
||||
{
|
||||
let performance_contract_address = &self
|
||||
.performance_contract_address()
|
||||
.ok_or_else(|| NyxdError::unavailable_contract_address("performance contract"))?;
|
||||
self.query_contract_smart(performance_contract_address, &query)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::nyxd::contract_traits::tests::IgnoreValue;
|
||||
|
||||
// it's enough that this compiles and clippy is happy about it
|
||||
#[allow(dead_code)]
|
||||
fn all_query_variants_are_covered<C: PerformanceQueryClient + Send + Sync>(
|
||||
client: C,
|
||||
msg: PerformanceQueryMsg,
|
||||
) {
|
||||
match msg {
|
||||
PerformanceQueryMsg::Admin {} => client.admin().ignore(),
|
||||
PerformanceQueryMsg::NodePerformance { epoch_id, node_id } => {
|
||||
client.get_node_performance(epoch_id, node_id).ignore()
|
||||
}
|
||||
PerformanceQueryMsg::NodePerformancePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_node_performance_paged(node_id, start_after, limit)
|
||||
.ignore(),
|
||||
PerformanceQueryMsg::NodeMeasurements { epoch_id, node_id } => {
|
||||
client.get_node_measurements(epoch_id, node_id).ignore()
|
||||
}
|
||||
PerformanceQueryMsg::EpochMeasurementsPaged {
|
||||
epoch_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_epoch_measurements_paged(epoch_id, start_after, limit)
|
||||
.ignore(),
|
||||
PerformanceQueryMsg::EpochPerformancePaged {
|
||||
epoch_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_epoch_performance_paged(epoch_id, start_after, limit)
|
||||
.ignore(),
|
||||
PerformanceQueryMsg::FullHistoricalPerformancePaged { start_after, limit } => client
|
||||
.get_full_historical_performance_paged(start_after, limit)
|
||||
.ignore(),
|
||||
PerformanceQueryMsg::NetworkMonitor { address } => client
|
||||
.get_network_monitor(&address.parse().unwrap())
|
||||
.ignore(),
|
||||
PerformanceQueryMsg::NetworkMonitorsPaged { start_after, limit } => client
|
||||
.get_network_monitors_paged(start_after, limit)
|
||||
.ignore(),
|
||||
PerformanceQueryMsg::RetiredNetworkMonitorsPaged { start_after, limit } => client
|
||||
.get_retired_network_monitors_paged(start_after, limit)
|
||||
.ignore(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::nyxd::coin::Coin;
|
||||
use crate::nyxd::contract_traits::NymContractsProvider;
|
||||
use crate::nyxd::cosmwasm_client::types::ExecuteResult;
|
||||
use crate::nyxd::cosmwasm_client::ContractResponseData;
|
||||
use crate::nyxd::error::NyxdError;
|
||||
use crate::nyxd::{Fee, SigningCosmWasmClient};
|
||||
use crate::signing::signer::OfflineSigner;
|
||||
use async_trait::async_trait;
|
||||
use nym_performance_contract_common::{
|
||||
EpochId, ExecuteMsg as PerformanceExecuteMsg, NodeId, NodePerformance,
|
||||
RemoveEpochMeasurementsResponse,
|
||||
};
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait PerformanceSigningClient {
|
||||
async fn execute_performance_contract(
|
||||
&self,
|
||||
fee: Option<Fee>,
|
||||
msg: PerformanceExecuteMsg,
|
||||
memo: String,
|
||||
funds: Vec<Coin>,
|
||||
) -> Result<ExecuteResult, NyxdError>;
|
||||
|
||||
async fn update_admin(
|
||||
&self,
|
||||
admin: String,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_performance_contract(
|
||||
fee,
|
||||
PerformanceExecuteMsg::UpdateAdmin { admin },
|
||||
"PerformanceContract::UpdateAdmin".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn submit_performance(
|
||||
&self,
|
||||
epoch: EpochId,
|
||||
data: NodePerformance,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_performance_contract(
|
||||
fee,
|
||||
PerformanceExecuteMsg::Submit { epoch, data },
|
||||
"PerformanceContract::Submit".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn batch_submit_performance(
|
||||
&self,
|
||||
epoch: EpochId,
|
||||
data: Vec<NodePerformance>,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_performance_contract(
|
||||
fee,
|
||||
PerformanceExecuteMsg::BatchSubmit { epoch, data },
|
||||
"PerformanceContract::BatchSubmit".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn authorise_network_monitor(
|
||||
&self,
|
||||
address: String,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_performance_contract(
|
||||
fee,
|
||||
PerformanceExecuteMsg::AuthoriseNetworkMonitor { address },
|
||||
"PerformanceContract::AuthoriseNetworkMonitor".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn retire_network_monitor(
|
||||
&self,
|
||||
address: String,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_performance_contract(
|
||||
fee,
|
||||
PerformanceExecuteMsg::RetireNetworkMonitor { address },
|
||||
"PerformanceContract::RetireNetworkMonitor".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn remove_node_measurements(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
node_id: NodeId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_performance_contract(
|
||||
fee,
|
||||
PerformanceExecuteMsg::RemoveNodeMeasurements { epoch_id, node_id },
|
||||
"PerformanceContract::RemoveNodeMeasurements".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn partial_remove_epoch_measurements(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_performance_contract(
|
||||
fee,
|
||||
PerformanceExecuteMsg::RemoveEpochMeasurements { epoch_id },
|
||||
"PerformanceContract::RemoveEpochMeasurements".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn remove_epoch_measurements(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<(), NyxdError> {
|
||||
loop {
|
||||
let execute_res = self
|
||||
.partial_remove_epoch_measurements(epoch_id, fee.clone())
|
||||
.await?;
|
||||
let response = execute_res
|
||||
.parse_singleton_json_contract_response::<RemoveEpochMeasurementsResponse>()?;
|
||||
if !response.additional_entries_to_remove_remaining {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl<C> PerformanceSigningClient for C
|
||||
where
|
||||
C: SigningCosmWasmClient + NymContractsProvider + Sync,
|
||||
NyxdError: From<<Self as OfflineSigner>::Error>,
|
||||
{
|
||||
async fn execute_performance_contract(
|
||||
&self,
|
||||
fee: Option<Fee>,
|
||||
msg: PerformanceExecuteMsg,
|
||||
memo: String,
|
||||
funds: Vec<Coin>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let performance_contract_address = &self
|
||||
.performance_contract_address()
|
||||
.ok_or_else(|| NyxdError::unavailable_contract_address("performance contract"))?;
|
||||
|
||||
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier())));
|
||||
|
||||
let signer_address = &self.signer_addresses()?[0];
|
||||
self.execute(
|
||||
signer_address,
|
||||
performance_contract_address,
|
||||
&msg,
|
||||
fee,
|
||||
memo,
|
||||
funds,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::nyxd::contract_traits::tests::IgnoreValue;
|
||||
use nym_performance_contract_common::ExecuteMsg;
|
||||
|
||||
// it's enough that this compiles and clippy is happy about it
|
||||
#[allow(dead_code)]
|
||||
fn all_execute_variants_are_covered<C: PerformanceSigningClient + Send + Sync>(
|
||||
client: C,
|
||||
msg: PerformanceExecuteMsg,
|
||||
) {
|
||||
match msg {
|
||||
PerformanceExecuteMsg::UpdateAdmin { admin } => {
|
||||
client.update_admin(admin, None).ignore()
|
||||
}
|
||||
PerformanceExecuteMsg::Submit { epoch, data } => {
|
||||
client.submit_performance(epoch, data, None).ignore()
|
||||
}
|
||||
PerformanceExecuteMsg::BatchSubmit { epoch, data } => {
|
||||
client.batch_submit_performance(epoch, data, None).ignore()
|
||||
}
|
||||
PerformanceExecuteMsg::AuthoriseNetworkMonitor { address } => {
|
||||
client.authorise_network_monitor(address, None).ignore()
|
||||
}
|
||||
PerformanceExecuteMsg::RetireNetworkMonitor { address } => {
|
||||
client.retire_network_monitor(address, None).ignore()
|
||||
}
|
||||
ExecuteMsg::RemoveNodeMeasurements { epoch_id, node_id } => client
|
||||
.remove_node_measurements(epoch_id, node_id, None)
|
||||
.ignore(),
|
||||
ExecuteMsg::RemoveEpochMeasurements { epoch_id } => client
|
||||
.partial_remove_epoch_measurements(epoch_id, None)
|
||||
.ignore(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ use tendermint_rpc::endpoint::broadcast;
|
||||
use tracing::error;
|
||||
|
||||
pub use cosmrs::abci::MsgResponse;
|
||||
use cosmwasm_std::from_json;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
pub fn parse_singleton_u32_from_contract_response(b: Vec<u8>) -> Result<u32, NyxdError> {
|
||||
if b.len() != 4 {
|
||||
@@ -73,6 +75,11 @@ pub fn parse_msg_responses(data: Bytes) -> Vec<MsgResponse> {
|
||||
|
||||
// requires there's a single response message
|
||||
pub trait ContractResponseData: Sized {
|
||||
fn parse_singleton_json_contract_response<T: DeserializeOwned>(&self) -> Result<T, NyxdError> {
|
||||
let b = self.to_singleton_contract_data()?;
|
||||
from_json(&b).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn parse_singleton_u32_contract_data(&self) -> Result<u32, NyxdError> {
|
||||
let b = self.to_singleton_contract_data()?;
|
||||
parse_singleton_u32_from_contract_response(b)
|
||||
|
||||
@@ -276,6 +276,10 @@ impl<C, S> NymContractsProvider for NyxdClient<C, S> {
|
||||
self.config.contracts.vesting_contract_address.as_ref()
|
||||
}
|
||||
|
||||
fn performance_contract_address(&self) -> Option<&AccountId> {
|
||||
self.config.contracts.performance_contract_address.as_ref()
|
||||
}
|
||||
|
||||
fn ecash_contract_address(&self) -> Option<&AccountId> {
|
||||
self.config.contracts.ecash_contract_address.as_ref()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "nym-contracts-common-testing"
|
||||
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]
|
||||
anyhow = { workspace = true }
|
||||
cosmwasm-std = { workspace = true }
|
||||
cw-storage-plus = { workspace = true }
|
||||
|
||||
serde = { workspace = true }
|
||||
rand_chacha = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
cw-multi-test = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,127 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_std::testing::{message_info, MockApi, MockQuerier, MockStorage};
|
||||
use cosmwasm_std::{
|
||||
coins, Addr, BankMsg, CosmosMsg, Empty, Env, MemoryStorage, MessageInfo, Order, OwnedDeps,
|
||||
Response, StdResult, Storage,
|
||||
};
|
||||
use cw_storage_plus::{KeyDeserialize, Map, Prefix, PrimaryKey};
|
||||
use rand::{RngCore, SeedableRng};
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
|
||||
pub const TEST_DENOM: &str = "unym";
|
||||
pub const TEST_PREFIX: &str = "n";
|
||||
|
||||
pub fn mock_api() -> MockApi {
|
||||
MockApi::default().with_prefix(TEST_PREFIX)
|
||||
}
|
||||
|
||||
pub fn mock_dependencies() -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>> {
|
||||
OwnedDeps {
|
||||
storage: MockStorage::default(),
|
||||
api: mock_api(),
|
||||
querier: MockQuerier::default(),
|
||||
custom_query_type: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn test_rng() -> ChaCha20Rng {
|
||||
let dummy_seed = [42u8; 32];
|
||||
rand_chacha::ChaCha20Rng::from_seed(dummy_seed)
|
||||
}
|
||||
|
||||
pub fn deps_with_balance(env: &Env) -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>> {
|
||||
let mut deps = mock_dependencies();
|
||||
deps.querier = MockQuerier::<Empty>::new(&[(
|
||||
env.contract.address.as_str(),
|
||||
coins(100000000000, TEST_DENOM).as_slice(),
|
||||
)]);
|
||||
deps
|
||||
}
|
||||
|
||||
pub fn generate_sorted_addresses(n: usize) -> Vec<Addr> {
|
||||
let mut rng = test_rng();
|
||||
let mut addrs = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
addrs.push(mock_api().addr_make(&format!("addr{i}{}", rng.next_u64())));
|
||||
}
|
||||
addrs.sort();
|
||||
addrs
|
||||
}
|
||||
|
||||
pub fn addr<S: AsRef<str>>(raw: S) -> Addr {
|
||||
mock_api().addr_make(raw.as_ref())
|
||||
}
|
||||
|
||||
pub fn sender<S: AsRef<str>>(raw: S) -> MessageInfo {
|
||||
message_info(&addr(raw), &[])
|
||||
}
|
||||
|
||||
pub trait ExtractBankMsg {
|
||||
fn unwrap_bank_msg(self) -> Option<BankMsg>;
|
||||
}
|
||||
|
||||
impl ExtractBankMsg for Response {
|
||||
fn unwrap_bank_msg(self) -> Option<BankMsg> {
|
||||
for msg in self.messages {
|
||||
match msg.msg {
|
||||
CosmosMsg::Bank(bank_msg) => return Some(bank_msg),
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub trait FullReader<'a> {
|
||||
type Key;
|
||||
type Value: Serialize + DeserializeOwned;
|
||||
|
||||
fn all_values(&self, store: &dyn Storage) -> StdResult<Vec<Self::Value>>;
|
||||
|
||||
fn all_key_values(&self, store: &dyn Storage) -> StdResult<Vec<(Self::Key, Self::Value)>>;
|
||||
}
|
||||
|
||||
impl<'a, K, T> FullReader<'a> for Map<K, T>
|
||||
where
|
||||
T: Serialize + DeserializeOwned,
|
||||
K: PrimaryKey<'a> + KeyDeserialize,
|
||||
K::Output: 'static,
|
||||
{
|
||||
type Key = K::Output;
|
||||
type Value = T;
|
||||
|
||||
fn all_values(&self, store: &dyn Storage) -> StdResult<Vec<Self::Value>> {
|
||||
self.range(store, None, None, Order::Ascending)
|
||||
.map(|record| record.map(|r| r.1))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn all_key_values(&self, store: &dyn Storage) -> StdResult<Vec<(Self::Key, Self::Value)>> {
|
||||
self.range(store, None, None, Order::Ascending).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, K, T, B> FullReader<'a> for Prefix<K, T, B>
|
||||
where
|
||||
K: KeyDeserialize + 'static,
|
||||
T: Serialize + DeserializeOwned,
|
||||
B: PrimaryKey<'a>,
|
||||
{
|
||||
type Key = K::Output;
|
||||
type Value = T;
|
||||
|
||||
fn all_values(&self, store: &dyn Storage) -> StdResult<Vec<Self::Value>> {
|
||||
self.range(store, None, None, Order::Ascending)
|
||||
.map(|record| record.map(|r| r.1))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn all_key_values(&self, store: &dyn Storage) -> StdResult<Vec<(Self::Key, Self::Value)>> {
|
||||
self.range(store, None, None, Order::Ascending).collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// those are all used exclusively for testing thus unwraps, et al. are allowed
|
||||
#![allow(clippy::unwrap_used)]
|
||||
#![allow(clippy::expect_used)]
|
||||
#![allow(clippy::panic)]
|
||||
|
||||
pub mod helpers;
|
||||
pub mod tester;
|
||||
|
||||
pub use helpers::*;
|
||||
pub use tester::*;
|
||||
@@ -0,0 +1,239 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{ContractTester, TestableNymContract};
|
||||
use cosmwasm_std::testing::{message_info, mock_env};
|
||||
use cosmwasm_std::{
|
||||
from_json, Addr, Coin, ContractInfo, Deps, DepsMut, Env, MessageInfo, Response, StdResult,
|
||||
Storage, Timestamp,
|
||||
};
|
||||
use cw_multi_test::{next_block, AppResponse, Executor};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use std::any::type_name;
|
||||
use std::fmt::Debug;
|
||||
|
||||
pub trait ContractOpts {
|
||||
type ExecuteMsg;
|
||||
type QueryMsg;
|
||||
type ContractError;
|
||||
|
||||
fn deps(&self) -> Deps<'_>;
|
||||
|
||||
fn deps_mut(&mut self) -> DepsMut<'_>;
|
||||
|
||||
fn env(&self) -> Env;
|
||||
|
||||
fn addr_make(&self, input: &str) -> Addr;
|
||||
|
||||
fn deps_mut_env(&mut self) -> (DepsMut<'_>, Env) {
|
||||
let env = self.env().clone();
|
||||
(self.deps_mut(), env)
|
||||
}
|
||||
|
||||
fn storage(&self) -> &dyn Storage;
|
||||
|
||||
fn storage_mut(&mut self) -> &mut dyn Storage;
|
||||
|
||||
fn read_from_contract_storage<T: DeserializeOwned>(&self, key: impl AsRef<[u8]>) -> Option<T>;
|
||||
|
||||
fn set_contract_storage(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>);
|
||||
|
||||
fn unchecked_read_from_contract_storage<T: DeserializeOwned>(
|
||||
&self,
|
||||
key: impl AsRef<[u8]>,
|
||||
) -> T {
|
||||
let typ = type_name::<T>();
|
||||
self.read_from_contract_storage(key)
|
||||
.unwrap_or_else(|| panic!("value of type {typ} not present in the storage"))
|
||||
}
|
||||
|
||||
fn execute_raw(
|
||||
&mut self,
|
||||
sender: Addr,
|
||||
message: Self::ExecuteMsg,
|
||||
) -> Result<Response, Self::ContractError> {
|
||||
self.execute_raw_with_balance(sender, &[], message)
|
||||
}
|
||||
|
||||
fn execute_raw_with_balance(
|
||||
&mut self,
|
||||
sender: Addr,
|
||||
coins: &[Coin],
|
||||
message: Self::ExecuteMsg,
|
||||
) -> Result<Response, Self::ContractError>;
|
||||
}
|
||||
|
||||
impl<C> ContractOpts for ContractTester<C>
|
||||
where
|
||||
C: TestableNymContract,
|
||||
{
|
||||
type ExecuteMsg = C::ExecuteMsg;
|
||||
type QueryMsg = C::QueryMsg;
|
||||
type ContractError = C::ContractError;
|
||||
|
||||
fn deps(&self) -> Deps<'_> {
|
||||
Deps {
|
||||
storage: &self.storage,
|
||||
api: self.app.api(),
|
||||
querier: self.app.wrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn deps_mut(&mut self) -> DepsMut<'_> {
|
||||
DepsMut {
|
||||
storage: &mut self.storage,
|
||||
api: self.app.api(),
|
||||
querier: self.app.wrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn env(&self) -> Env {
|
||||
Env {
|
||||
block: self.app.block_info(),
|
||||
contract: ContractInfo {
|
||||
address: self.contract_address.clone(),
|
||||
},
|
||||
..mock_env()
|
||||
}
|
||||
}
|
||||
|
||||
fn addr_make(&self, input: &str) -> Addr {
|
||||
self.app.api().addr_make(input)
|
||||
}
|
||||
|
||||
fn storage(&self) -> &dyn Storage {
|
||||
&self.storage
|
||||
}
|
||||
|
||||
fn storage_mut(&mut self) -> &mut dyn Storage {
|
||||
&mut self.storage
|
||||
}
|
||||
|
||||
fn read_from_contract_storage<T: DeserializeOwned>(&self, key: impl AsRef<[u8]>) -> Option<T> {
|
||||
let raw = self.deps().storage.get(key.as_ref())?;
|
||||
from_json(&raw).ok()
|
||||
}
|
||||
|
||||
fn set_contract_storage(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>) {
|
||||
self.deps_mut().storage.set(key.as_ref(), value.as_ref());
|
||||
}
|
||||
|
||||
fn execute_raw_with_balance(
|
||||
&mut self,
|
||||
sender: Addr,
|
||||
coins: &[Coin],
|
||||
message: C::ExecuteMsg,
|
||||
) -> Result<Response, C::ContractError> {
|
||||
let env = self.env();
|
||||
let info = message_info(&sender, coins);
|
||||
|
||||
C::execute()(self.deps_mut(), env, info, message)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ChainOpts: ContractOpts {
|
||||
fn set_contract_balance(&mut self, balance: Coin);
|
||||
|
||||
fn next_block(&mut self);
|
||||
|
||||
fn set_block_time(&mut self, time: Timestamp);
|
||||
|
||||
fn execute_msg(
|
||||
&mut self,
|
||||
sender: Addr,
|
||||
message: &Self::ExecuteMsg,
|
||||
) -> anyhow::Result<AppResponse> {
|
||||
self.execute_msg_with_balance(sender, &[], message)
|
||||
}
|
||||
|
||||
fn execute_msg_with_balance(
|
||||
&mut self,
|
||||
sender: Addr,
|
||||
coins: &[Coin],
|
||||
message: &Self::ExecuteMsg,
|
||||
) -> anyhow::Result<AppResponse>;
|
||||
|
||||
fn execute_arbitrary_contract<T: Serialize + Debug>(
|
||||
&mut self,
|
||||
contract: Addr,
|
||||
sender: MessageInfo,
|
||||
message: &T,
|
||||
) -> anyhow::Result<AppResponse>;
|
||||
|
||||
fn query_arbitrary_contract<Q: Serialize + Debug, T: DeserializeOwned>(
|
||||
&self,
|
||||
contract: Addr,
|
||||
message: &Q,
|
||||
) -> StdResult<T>;
|
||||
|
||||
fn query<T: DeserializeOwned>(&self, message: &Self::QueryMsg) -> StdResult<T>;
|
||||
}
|
||||
|
||||
impl<C> ChainOpts for ContractTester<C>
|
||||
where
|
||||
C: TestableNymContract,
|
||||
{
|
||||
fn set_contract_balance(&mut self, balance: Coin) {
|
||||
let contract_address = &self.contract_address;
|
||||
self.app
|
||||
.router()
|
||||
.bank
|
||||
.init_balance(
|
||||
&mut self.storage.inner_storage(),
|
||||
contract_address,
|
||||
vec![balance],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
fn next_block(&mut self) {
|
||||
self.app.update_block(next_block)
|
||||
}
|
||||
|
||||
fn set_block_time(&mut self, time: Timestamp) {
|
||||
self.app.update_block(|b| b.time = time)
|
||||
}
|
||||
|
||||
fn execute_msg(
|
||||
&mut self,
|
||||
sender: Addr,
|
||||
message: &C::ExecuteMsg,
|
||||
) -> anyhow::Result<AppResponse> {
|
||||
self.execute_msg_with_balance(sender, &[], message)
|
||||
}
|
||||
|
||||
fn execute_msg_with_balance(
|
||||
&mut self,
|
||||
sender: Addr,
|
||||
coins: &[Coin],
|
||||
message: &C::ExecuteMsg,
|
||||
) -> anyhow::Result<AppResponse> {
|
||||
self.app
|
||||
.execute_contract(sender, self.contract_address.clone(), message, coins)
|
||||
}
|
||||
|
||||
fn execute_arbitrary_contract<T: Serialize + Debug>(
|
||||
&mut self,
|
||||
contract: Addr,
|
||||
sender: MessageInfo,
|
||||
message: &T,
|
||||
) -> anyhow::Result<AppResponse> {
|
||||
let coins = &sender.funds;
|
||||
let sender = sender.sender;
|
||||
self.app.execute_contract(sender, contract, message, coins)
|
||||
}
|
||||
|
||||
fn query_arbitrary_contract<Q: Serialize + Debug, T: DeserializeOwned>(
|
||||
&self,
|
||||
contract: Addr,
|
||||
message: &Q,
|
||||
) -> StdResult<T> {
|
||||
self.app.wrap().query_wasm_smart(contract, message)
|
||||
}
|
||||
|
||||
fn query<T: DeserializeOwned>(&self, message: &C::QueryMsg) -> StdResult<T> {
|
||||
self.app
|
||||
.wrap()
|
||||
.query_wasm_smart(self.contract_address.as_str(), message)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{
|
||||
CommonStorageKeys, ContractOpts, ContractTester, StorageWrapper, TestableNymContract,
|
||||
TEST_DENOM,
|
||||
};
|
||||
use cosmwasm_std::testing::message_info;
|
||||
use cosmwasm_std::{
|
||||
coin, coins, from_json, to_json_vec, Addr, Coin, MessageInfo, StdError, StdResult, Storage,
|
||||
};
|
||||
use cw_multi_test::Executor;
|
||||
use cw_storage_plus::{Key, Path, PrimaryKey};
|
||||
use rand::RngCore;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use std::any::type_name;
|
||||
use std::ops::Deref;
|
||||
|
||||
pub trait StorageReader {
|
||||
fn common_key(&self, key: CommonStorageKeys) -> Option<&[u8]>;
|
||||
|
||||
fn read_common_value<T: DeserializeOwned>(&self, key: CommonStorageKeys) -> Option<T> {
|
||||
self.read_from_contract_storage(self.common_key(key)?)
|
||||
}
|
||||
|
||||
fn unchecked_read_common_value<T: DeserializeOwned>(&self, key: CommonStorageKeys) -> T {
|
||||
self.unchecked_read_from_contract_storage(
|
||||
self.common_key(key)
|
||||
.unwrap_or_else(|| panic!("no key set for {key:?}")),
|
||||
)
|
||||
}
|
||||
|
||||
fn read_from_contract_storage<T: DeserializeOwned>(&self, key: impl AsRef<[u8]>) -> Option<T>;
|
||||
|
||||
fn unchecked_read_from_contract_storage<T: DeserializeOwned>(
|
||||
&self,
|
||||
key: impl AsRef<[u8]>,
|
||||
) -> T {
|
||||
let typ = type_name::<T>();
|
||||
self.read_from_contract_storage(key)
|
||||
.unwrap_or_else(|| panic!("value of type {typ} not present in the storage"))
|
||||
}
|
||||
}
|
||||
|
||||
// technically it shouldn't rely on `StorageReader` and `common_key` should be extracted
|
||||
// but this makes it a tad easier and it's only testing code so it's fine
|
||||
pub trait StorageWriter: StorageReader {
|
||||
fn set_common_value<T: Serialize>(
|
||||
&mut self,
|
||||
key: CommonStorageKeys,
|
||||
value: &T,
|
||||
) -> StdResult<()> {
|
||||
let key = self
|
||||
.common_key(key)
|
||||
.ok_or(StdError::not_found("key not found"))?
|
||||
.to_vec();
|
||||
self.set_storage_value(key, value)
|
||||
}
|
||||
|
||||
fn set_storage(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>);
|
||||
|
||||
fn set_storage_value<T: Serialize>(
|
||||
&mut self,
|
||||
key: impl AsRef<[u8]>,
|
||||
value: &T,
|
||||
) -> StdResult<()> {
|
||||
self.set_storage(key, &to_json_vec(value)?);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ArbitraryContractStorageReader {
|
||||
fn may_read_from_contract_storage(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
key: impl AsRef<[u8]>,
|
||||
) -> Option<Vec<u8>>;
|
||||
|
||||
fn must_read_from_contract_storage(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
key: impl AsRef<[u8]>,
|
||||
) -> StdResult<Vec<u8>> {
|
||||
let key = key.as_ref();
|
||||
self.may_read_from_contract_storage(address, key)
|
||||
.ok_or(StdError::not_found(format!("no data under {key:?}")))
|
||||
}
|
||||
|
||||
fn may_read_value_from_contract_storage<T: DeserializeOwned>(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
key: impl AsRef<[u8]>,
|
||||
) -> StdResult<Option<T>> {
|
||||
let Some(bytes) = self.may_read_from_contract_storage(address, key) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
from_json(&bytes).map(Some)
|
||||
}
|
||||
|
||||
fn must_read_value_from_contract_storage<T: DeserializeOwned>(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
key: impl AsRef<[u8]>,
|
||||
) -> StdResult<T> {
|
||||
let bytes = self.must_read_from_contract_storage(address, key)?;
|
||||
from_json(&bytes)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ArbitraryContractStorageWriter {
|
||||
fn set_contract_storage(
|
||||
&mut self,
|
||||
address: impl Into<String>,
|
||||
key: impl AsRef<[u8]>,
|
||||
value: impl AsRef<[u8]>,
|
||||
);
|
||||
|
||||
fn set_contract_storage_value<T: Serialize>(
|
||||
&mut self,
|
||||
address: impl Into<String>,
|
||||
key: impl AsRef<[u8]>,
|
||||
value: &T,
|
||||
) -> StdResult<()> {
|
||||
self.set_contract_storage(address, key, &to_json_vec(value)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// attempts to write to an arbitrary contract `cw_storage_plus::Map`
|
||||
fn set_contract_map_value<'a, K, T>(
|
||||
&mut self,
|
||||
address: impl Into<String>,
|
||||
namespace: impl AsRef<[u8]>,
|
||||
key: K,
|
||||
value: &T,
|
||||
) -> StdResult<()>
|
||||
where
|
||||
K: PrimaryKey<'a>,
|
||||
T: Serialize + DeserializeOwned,
|
||||
{
|
||||
let key_path: Path<T> = Path::new(
|
||||
namespace.as_ref(),
|
||||
&key.key().iter().map(Key::as_ref).collect::<Vec<_>>(),
|
||||
);
|
||||
let storage_key = key_path.deref();
|
||||
self.set_contract_storage_value(address, storage_key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// contract that has an admin
|
||||
pub trait AdminExt: StorageReader + StorageWriter {
|
||||
fn admin(&self) -> Option<Addr> {
|
||||
self.read_common_value(CommonStorageKeys::Admin)
|
||||
}
|
||||
|
||||
fn update_admin(&mut self, admin: &Option<Addr>) -> StdResult<()> {
|
||||
self.set_common_value(CommonStorageKeys::Admin, admin)
|
||||
}
|
||||
|
||||
fn admin_unchecked(&self) -> Addr {
|
||||
self.admin().expect("no admin set")
|
||||
}
|
||||
|
||||
fn admin_msg(&self) -> MessageInfo {
|
||||
message_info(&self.admin_unchecked(), &[])
|
||||
}
|
||||
}
|
||||
|
||||
// contract that operates on some specific coin denom
|
||||
pub trait DenomExt: StorageReader {
|
||||
fn denom(&self) -> String {
|
||||
self.unchecked_read_common_value(CommonStorageKeys::Denom)
|
||||
}
|
||||
|
||||
fn coin(&self, amount: u128) -> Coin {
|
||||
coin(amount, self.denom())
|
||||
}
|
||||
|
||||
fn coins(&self, amount: u128) -> Vec<Coin> {
|
||||
coins(amount, self.denom())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RandExt {
|
||||
fn raw_rng(&mut self) -> &mut ChaCha20Rng;
|
||||
|
||||
fn generate_account(&mut self) -> Addr;
|
||||
|
||||
fn generate_account_with_balance(&mut self) -> Addr
|
||||
where
|
||||
Self: BankExt;
|
||||
}
|
||||
|
||||
pub trait BankExt {
|
||||
fn send_tokens(&mut self, to: Addr, amount: Coin) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
impl<T> AdminExt for T where T: StorageReader + StorageWriter {}
|
||||
impl<T> DenomExt for T where T: StorageReader {}
|
||||
|
||||
impl<C: TestableNymContract> StorageReader for ContractTester<C> {
|
||||
fn common_key(&self, key: CommonStorageKeys) -> Option<&[u8]> {
|
||||
self.common_storage_keys.get(&key).map(|v| &**v)
|
||||
}
|
||||
|
||||
fn read_from_contract_storage<T: DeserializeOwned>(&self, key: impl AsRef<[u8]>) -> Option<T> {
|
||||
<Self as ContractOpts>::read_from_contract_storage(self, key)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: TestableNymContract> StorageWriter for ContractTester<C> {
|
||||
fn set_storage(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>) {
|
||||
<Self as ContractOpts>::set_contract_storage(self, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: TestableNymContract> BankExt for ContractTester<C> {
|
||||
fn send_tokens(&mut self, to: Addr, amount: Coin) -> anyhow::Result<()> {
|
||||
self.app
|
||||
.send_tokens(self.master_address.clone(), to, &[amount])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: TestableNymContract> RandExt for ContractTester<C> {
|
||||
fn raw_rng(&mut self) -> &mut ChaCha20Rng {
|
||||
&mut self.rng
|
||||
}
|
||||
|
||||
fn generate_account(&mut self) -> Addr {
|
||||
self.app
|
||||
.api()
|
||||
.addr_make(&format!("foomp{}", self.rng.next_u64()))
|
||||
}
|
||||
|
||||
fn generate_account_with_balance(&mut self) -> Addr
|
||||
where
|
||||
Self: BankExt,
|
||||
{
|
||||
let addr = self.generate_account();
|
||||
let million = 1_000_000_000_000;
|
||||
self.send_tokens(addr.clone(), coin(million, TEST_DENOM))
|
||||
.unwrap();
|
||||
addr
|
||||
}
|
||||
}
|
||||
|
||||
impl ArbitraryContractStorageReader for StorageWrapper {
|
||||
fn may_read_from_contract_storage(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
key: impl AsRef<[u8]>,
|
||||
) -> Option<Vec<u8>> {
|
||||
self.contract_storage_wrapper(&Addr::unchecked(address))
|
||||
.get(key.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl ArbitraryContractStorageWriter for StorageWrapper {
|
||||
fn set_contract_storage(
|
||||
&mut self,
|
||||
address: impl Into<String>,
|
||||
key: impl AsRef<[u8]>,
|
||||
value: impl AsRef<[u8]>,
|
||||
) {
|
||||
// yeah, we're unnecessarily cloning a Rc pointer, but this is a test code, so this inefficiency is fine
|
||||
let mut wrapped_storage = self
|
||||
.clone()
|
||||
.contract_storage_wrapper(&Addr::unchecked(address));
|
||||
wrapped_storage.set(key.as_ref(), value.as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> ArbitraryContractStorageReader for ContractTester<C>
|
||||
where
|
||||
C: TestableNymContract,
|
||||
{
|
||||
fn may_read_from_contract_storage(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
key: impl AsRef<[u8]>,
|
||||
) -> Option<Vec<u8>> {
|
||||
self.storage
|
||||
.as_inner_storage()
|
||||
.may_read_from_contract_storage(address, key)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> ArbitraryContractStorageWriter for ContractTester<C>
|
||||
where
|
||||
C: TestableNymContract,
|
||||
{
|
||||
fn set_contract_storage(
|
||||
&mut self,
|
||||
address: impl Into<String>,
|
||||
key: impl AsRef<[u8]>,
|
||||
value: impl AsRef<[u8]>,
|
||||
) {
|
||||
self.storage
|
||||
.as_inner_storage_mut()
|
||||
.set_contract_storage(address, key, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{mock_api, test_rng, TEST_DENOM};
|
||||
use cosmwasm_std::testing::MockApi;
|
||||
use cosmwasm_std::{
|
||||
coin, coins, Addr, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Order, QuerierWrapper,
|
||||
Record, Response, Storage,
|
||||
};
|
||||
use cw_multi_test::{App, AppBuilder, BankKeeper, Contract, ContractWrapper, Executor};
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
pub use basic_traits::*;
|
||||
pub use extensions::*;
|
||||
|
||||
pub use crate::tester::storage_wrapper::{ContractStorageWrapper, StorageWrapper};
|
||||
|
||||
mod basic_traits;
|
||||
mod extensions;
|
||||
mod storage_wrapper;
|
||||
// copied from cw-multi-test (but removed generics for custom messages and querier for we don't need them for now)
|
||||
|
||||
pub type ContractFn<T, E> =
|
||||
fn(deps: DepsMut, env: Env, info: MessageInfo, msg: T) -> Result<Response, E>;
|
||||
pub type QueryFn<T, E> = fn(deps: Deps, env: Env, msg: T) -> Result<Binary, E>;
|
||||
pub type PermissionedFn<T, E> = fn(deps: DepsMut, env: Env, msg: T) -> Result<Response, E>;
|
||||
|
||||
pub type ContractClosure<T, E> = Box<dyn Fn(DepsMut, Env, MessageInfo, T) -> Result<Response, E>>;
|
||||
pub type QueryClosure<T, E> = Box<dyn Fn(Deps, Env, T) -> Result<Binary, E>>;
|
||||
|
||||
pub trait TestableNymContract {
|
||||
const NAME: &'static str;
|
||||
|
||||
type InitMsg: DeserializeOwned + Serialize + Debug + 'static;
|
||||
type ExecuteMsg: DeserializeOwned + Serialize + Debug + 'static;
|
||||
type QueryMsg: DeserializeOwned + Serialize + Debug + 'static;
|
||||
type MigrateMsg: DeserializeOwned + Serialize + Debug + 'static;
|
||||
type ContractError: Display + Debug + Send + Sync + 'static;
|
||||
|
||||
fn instantiate() -> ContractFn<Self::InitMsg, Self::ContractError>;
|
||||
fn execute() -> ContractFn<Self::ExecuteMsg, Self::ContractError>;
|
||||
fn query() -> QueryFn<Self::QueryMsg, Self::ContractError>;
|
||||
fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError>;
|
||||
|
||||
fn base_init_msg() -> Self::InitMsg;
|
||||
|
||||
// // for now we don't care about custom queriers
|
||||
// fn contract_wrapper() -> ContractWrapper<
|
||||
// Self::ExecuteMsg,
|
||||
// Self::InitMsg,
|
||||
// Self::QueryMsg,
|
||||
// Self::ContractError,
|
||||
// anyhow::Error,
|
||||
// anyhow::Error,
|
||||
// Empty,
|
||||
// Empty,
|
||||
// Empty,
|
||||
// Self::ContractError,
|
||||
// Self::ContractError,
|
||||
// Self::MigrateMsg,
|
||||
// Self::ContractError,
|
||||
// > {
|
||||
// ContractWrapper::new(Self::execute(), Self::instantiate(), Self::query())
|
||||
// .with_migrate(Self::migrate())
|
||||
// }
|
||||
|
||||
fn dyn_contract() -> Box<dyn Contract<Empty>> {
|
||||
Box::new(
|
||||
ContractWrapper::new(Self::execute(), Self::instantiate(), Self::query())
|
||||
.with_migrate(Self::migrate()),
|
||||
)
|
||||
}
|
||||
|
||||
fn init() -> ContractTester<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
ContractTesterBuilder::new()
|
||||
.instantiate::<Self>(None)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ContractTesterBuilder<C> {
|
||||
contract: PhantomData<C>,
|
||||
master_address: Addr,
|
||||
app: App<BankKeeper, MockApi, StorageWrapper>,
|
||||
storage: StorageWrapper,
|
||||
pub well_known_contracts: HashMap<&'static str, Addr>,
|
||||
}
|
||||
|
||||
impl<C> ContractTesterBuilder<C> {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self
|
||||
where
|
||||
C: TestableNymContract,
|
||||
{
|
||||
let storage = StorageWrapper::new();
|
||||
|
||||
let api = mock_api();
|
||||
let master_address = api.addr_make("master-owner");
|
||||
|
||||
let app = AppBuilder::new()
|
||||
.with_api(api)
|
||||
.with_storage(storage.clone())
|
||||
.build(|router, _api, storage| {
|
||||
router
|
||||
.bank
|
||||
.init_balance(
|
||||
storage,
|
||||
&master_address,
|
||||
coins(1000000000000000, TEST_DENOM),
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
ContractTesterBuilder {
|
||||
contract: Default::default(),
|
||||
master_address,
|
||||
app,
|
||||
storage,
|
||||
well_known_contracts: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn instantiate<D: TestableNymContract>(
|
||||
mut self,
|
||||
custom_init_msg: Option<D::InitMsg>,
|
||||
) -> ContractTesterBuilder<C> {
|
||||
let code_id = self.app.store_code(D::dyn_contract());
|
||||
let contract_address = self
|
||||
.app
|
||||
.instantiate_contract(
|
||||
code_id,
|
||||
self.master_address.clone(),
|
||||
&custom_init_msg.unwrap_or(D::base_init_msg()),
|
||||
&[],
|
||||
D::NAME,
|
||||
Some(self.master_address.to_string()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// send some tokens to the contract
|
||||
self.app
|
||||
.send_tokens(
|
||||
self.master_address.clone(),
|
||||
contract_address.clone(),
|
||||
&[coin(100000000, TEST_DENOM)],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
self.well_known_contracts.insert(D::NAME, contract_address);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> ContractTester<C>
|
||||
where
|
||||
C: TestableNymContract,
|
||||
{
|
||||
if !self.well_known_contracts.contains_key(C::NAME) {
|
||||
panic!("{} contract has not been instantiated", C::NAME);
|
||||
}
|
||||
|
||||
let contract_address = self.well_known_contracts[C::NAME].clone();
|
||||
|
||||
ContractTester {
|
||||
contract: self.contract,
|
||||
app: self.app,
|
||||
rng: test_rng(),
|
||||
master_address: self.master_address,
|
||||
storage: self.storage.contract_storage_wrapper(&contract_address),
|
||||
contract_address,
|
||||
common_storage_keys: Default::default(),
|
||||
well_known_contracts: self.well_known_contracts,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contract_storage_wrapper(&self, contract: &Addr) -> ContractStorageWrapper {
|
||||
self.storage.contract_storage_wrapper(contract)
|
||||
}
|
||||
|
||||
pub fn api(&self) -> MockApi {
|
||||
*self.app.api()
|
||||
}
|
||||
|
||||
pub fn querier(&self) -> QuerierWrapper {
|
||||
self.app.wrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
|
||||
pub enum CommonStorageKeys {
|
||||
Admin,
|
||||
Denom,
|
||||
}
|
||||
|
||||
pub struct ContractTester<C: TestableNymContract> {
|
||||
contract: PhantomData<C>,
|
||||
pub app: App<BankKeeper, MockApi, StorageWrapper>,
|
||||
pub rng: ChaCha20Rng,
|
||||
pub contract_address: Addr,
|
||||
pub master_address: Addr,
|
||||
pub(crate) storage: ContractStorageWrapper,
|
||||
pub common_storage_keys: HashMap<CommonStorageKeys, Vec<u8>>,
|
||||
|
||||
// TODO: limitation: doesn't allow multiple contracts of the same type (but that's fine for the time being)
|
||||
pub well_known_contracts: HashMap<&'static str, Addr>,
|
||||
}
|
||||
|
||||
impl<C> ContractTester<C>
|
||||
where
|
||||
C: TestableNymContract,
|
||||
{
|
||||
pub fn insert_common_storage_key(&mut self, key: CommonStorageKeys, value: impl AsRef<[u8]>) {
|
||||
self.common_storage_keys
|
||||
.insert(key, value.as_ref().to_vec());
|
||||
}
|
||||
|
||||
pub fn with_common_storage_key(
|
||||
mut self,
|
||||
key: CommonStorageKeys,
|
||||
value: impl AsRef<[u8]>,
|
||||
) -> Self {
|
||||
self.insert_common_storage_key(key, value);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> Storage for ContractTester<C>
|
||||
where
|
||||
C: TestableNymContract,
|
||||
{
|
||||
fn get(&self, key: &[u8]) -> Option<Vec<u8>> {
|
||||
self.storage.get(key)
|
||||
}
|
||||
|
||||
fn range<'a>(
|
||||
&'a self,
|
||||
start: Option<&[u8]>,
|
||||
end: Option<&[u8]>,
|
||||
order: Order,
|
||||
) -> Box<dyn Iterator<Item = Record> + 'a> {
|
||||
self.storage.range(start, end, order)
|
||||
}
|
||||
|
||||
fn range_keys<'a>(
|
||||
&'a self,
|
||||
start: Option<&[u8]>,
|
||||
end: Option<&[u8]>,
|
||||
order: Order,
|
||||
) -> Box<dyn Iterator<Item = Vec<u8>> + 'a> {
|
||||
self.storage.range_keys(start, end, order)
|
||||
}
|
||||
|
||||
fn range_values<'a>(
|
||||
&'a self,
|
||||
start: Option<&[u8]>,
|
||||
end: Option<&[u8]>,
|
||||
order: Order,
|
||||
) -> Box<dyn Iterator<Item = Vec<u8>> + 'a> {
|
||||
self.storage.range_values(start, end, order)
|
||||
}
|
||||
|
||||
fn set(&mut self, key: &[u8], value: &[u8]) {
|
||||
self.storage.set(key, value)
|
||||
}
|
||||
|
||||
fn remove(&mut self, key: &[u8]) {
|
||||
self.storage.remove(key)
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ use std::rc::Rc;
|
||||
pub struct StorageWrapper(Rc<RefCell<MemoryStorage>>);
|
||||
|
||||
impl StorageWrapper {
|
||||
pub(super) fn contract_storage_wrapper(&self, contract: &Addr) -> ContractStorageWrapper {
|
||||
pub fn contract_storage_wrapper(&self, contract: &Addr) -> ContractStorageWrapper {
|
||||
ContractStorageWrapper {
|
||||
address: contract.clone(),
|
||||
inner: self.clone(),
|
||||
@@ -24,7 +24,7 @@ impl StorageWrapper {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ContractStorageWrapper {
|
||||
pub struct ContractStorageWrapper {
|
||||
address: Addr,
|
||||
inner: StorageWrapper,
|
||||
}
|
||||
@@ -33,6 +33,22 @@ impl ContractStorageWrapper {
|
||||
pub fn inner_storage(&self) -> StorageWrapper {
|
||||
self.inner.clone()
|
||||
}
|
||||
|
||||
pub fn as_inner_storage(&self) -> &StorageWrapper {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
pub fn as_inner_storage_mut(&mut self) -> &mut StorageWrapper {
|
||||
&mut self.inner
|
||||
}
|
||||
|
||||
#[must_use = "this returns the result of the operation, without modifying the original"]
|
||||
pub fn change_contract(&self, contract: &Addr) -> Self {
|
||||
ContractStorageWrapper {
|
||||
address: contract.clone(),
|
||||
inner: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Storage for StorageWrapper {
|
||||
@@ -18,6 +18,7 @@ serde = { workspace = true, features = ["derive"] }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
|
||||
@@ -35,7 +35,7 @@ pub enum ContractsCommonError {
|
||||
/// Percent represents a value between 0 and 100%
|
||||
/// (i.e. between 0.0 and 1.0)
|
||||
#[cw_serde]
|
||||
#[derive(Copy, Default, PartialOrd)]
|
||||
#[derive(Copy, Default, PartialOrd, Ord, Eq)]
|
||||
pub struct Percent(#[serde(deserialize_with = "de_decimal_percent")] Decimal);
|
||||
|
||||
impl Percent {
|
||||
@@ -80,6 +80,44 @@ impl Percent {
|
||||
pub fn checked_pow(&self, exp: u32) -> Result<Self, OverflowError> {
|
||||
self.0.checked_pow(exp).map(Percent)
|
||||
}
|
||||
|
||||
// truncate provided percent to only have 2 decimal places,
|
||||
// e.g. convert "0.1234567" into "0.12"
|
||||
// the purpose of it is to reduce storage space, in particular for the performance contract
|
||||
// since that extra precision gains us nothing
|
||||
#[must_use = "this returns the result of the operation, without modifying the original"]
|
||||
pub fn round_to_two_decimal_places(&self) -> Self {
|
||||
let raw = self.0;
|
||||
|
||||
const DECIMAL_FRACTIONAL: Uint128 = Uint128::new(1_000_000_000_000_000_000u128); // 1*10**18
|
||||
const THRESHOLD: Decimal = Decimal::permille(5); // 0.005
|
||||
|
||||
// in case it ever changes since it's not exposed in the public API
|
||||
debug_assert_eq!(
|
||||
DECIMAL_FRACTIONAL,
|
||||
Uint128::new(10).pow(Decimal::DECIMAL_PLACES)
|
||||
);
|
||||
|
||||
let int = (raw.atomics() * Uint128::new(100)) / DECIMAL_FRACTIONAL;
|
||||
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let floored = Decimal::from_atomics(int, 2).unwrap();
|
||||
let diff = raw - floored;
|
||||
let rounded = if diff >= THRESHOLD {
|
||||
// ceil
|
||||
floored + Decimal::percent(1)
|
||||
} else {
|
||||
floored
|
||||
};
|
||||
Percent(rounded)
|
||||
}
|
||||
|
||||
#[must_use = "this returns the result of the operation, without modifying the original"]
|
||||
pub fn average(&self, other: &Self) -> Self {
|
||||
let sum = self.0 + other.0;
|
||||
let inner = Decimal::from_ratio(sum.numerator(), sum.denominator() * Uint128::new(2));
|
||||
Percent(inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Percent {
|
||||
@@ -334,6 +372,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "naive_float")]
|
||||
fn naive_float_conversion() {
|
||||
// around 15 decimal places is the maximum precision we can handle
|
||||
// which is still way more than enough for what we use it for
|
||||
@@ -347,4 +386,41 @@ mod tests {
|
||||
|
||||
assert!(converted.0 - converted.0 < epsilon);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rounding_percent() {
|
||||
let test_cases = vec![
|
||||
("0", "0"),
|
||||
("0.1", "0.1"),
|
||||
("0.12", "0.12"),
|
||||
("0.12", "0.123"),
|
||||
("0.12", "0.123456789"),
|
||||
("0.13", "0.125"),
|
||||
("0.13", "0.126"),
|
||||
("0.13", "0.126436545676"),
|
||||
("0.99", "0.99"),
|
||||
("0.99", "0.994"),
|
||||
("1", "0.999"),
|
||||
("1", "0.995"),
|
||||
];
|
||||
for (expected, input) in test_cases {
|
||||
let expected: Percent = expected.parse().unwrap();
|
||||
let pre_truncated: Percent = input.parse().unwrap();
|
||||
assert_eq!(expected, pre_truncated.round_to_two_decimal_places())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calculating_average() -> anyhow::Result<()> {
|
||||
fn p(raw: &str) -> Percent {
|
||||
raw.parse().unwrap()
|
||||
}
|
||||
|
||||
assert_eq!(p("0.1").average(&p("0.1")), p("0.1"));
|
||||
assert_eq!(p("0.1").average(&p("0.2")), p("0.15"));
|
||||
assert_eq!(p("1").average(&p("0")), p("0.5"));
|
||||
assert_eq!(p("0.123").average(&p("0.456")), p("0.2895"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ semver = { workspace = true, features = ["serde"] }
|
||||
schemars = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
contracts-common = { path = "../contracts-common", package = "nym-contracts-common", version = "0.5.0" }
|
||||
serde-json-wasm = { workspace = true }
|
||||
humantime-serde = { workspace = true }
|
||||
utoipa = { workspace = true, optional = true }
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use crate::{IdentityKey, NodeId, SphinxKey};
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use cosmwasm_std::{Addr, Coin};
|
||||
use cosmwasm_std::{to_json_string, Addr, Coin};
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt::Display;
|
||||
|
||||
@@ -154,7 +154,7 @@ pub struct GatewayConfigUpdate {
|
||||
|
||||
impl GatewayConfigUpdate {
|
||||
pub fn to_inline_json(&self) -> String {
|
||||
serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||
to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::{
|
||||
Percent, ProfitMarginRange, SphinxKey,
|
||||
};
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use cosmwasm_std::{Addr, Coin, Decimal, StdResult, Uint128};
|
||||
use cosmwasm_std::{to_json_string, Addr, Coin, Decimal, StdResult, Uint128};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
@@ -604,7 +604,7 @@ pub struct NodeCostParams {
|
||||
|
||||
impl NodeCostParams {
|
||||
pub fn to_inline_json(&self) -> String {
|
||||
serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||
to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -773,7 +773,7 @@ pub struct MixNodeConfigUpdate {
|
||||
|
||||
impl MixNodeConfigUpdate {
|
||||
pub fn to_inline_json(&self) -> String {
|
||||
serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||
to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::helpers::IntoBaseDecimal;
|
||||
use crate::nym_node::Role;
|
||||
use crate::{error::MixnetContractError, Percent};
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use cosmwasm_std::Decimal;
|
||||
use cosmwasm_std::{to_json_string, Decimal};
|
||||
|
||||
pub type Performance = Percent;
|
||||
pub type WorkFactor = Decimal;
|
||||
@@ -84,7 +84,7 @@ pub struct IntervalRewardParams {
|
||||
|
||||
impl IntervalRewardParams {
|
||||
pub fn to_inline_json(&self) -> String {
|
||||
serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||
to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,7 +410,7 @@ impl IntervalRewardingParamsUpdate {
|
||||
}
|
||||
|
||||
pub fn to_inline_json(&self) -> String {
|
||||
serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||
to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "nym-performance-contract-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]
|
||||
thiserror = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
|
||||
cosmwasm-std = { workspace = true }
|
||||
cosmwasm-schema = { workspace = true }
|
||||
cw-controllers = { workspace = true }
|
||||
|
||||
nym-contracts-common = { path = "../contracts-common" }
|
||||
|
||||
|
||||
[features]
|
||||
schema = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod storage_keys {
|
||||
pub const CONTRACT_ADMIN: &str = "contract-admin";
|
||||
pub const INITIAL_EPOCH_ID: &str = "initial-epoch-id";
|
||||
pub const MIXNET_CONTRACT: &str = "mixnet-contract";
|
||||
pub const AUTHORISED_COUNT: &str = "authorised-count";
|
||||
pub const AUTHORISED: &str = "authorised";
|
||||
pub const RETIRED: &str = "retired";
|
||||
pub const PERFORMANCE_RESULTS: &str = "performance-results";
|
||||
pub const SUBMISSION_METADATA: &str = "submission-metadata";
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{EpochId, NodeId};
|
||||
use cosmwasm_std::Addr;
|
||||
use cw_controllers::AdminError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug, PartialEq)]
|
||||
pub enum NymPerformanceContractError {
|
||||
#[error("could not perform contract migration: {comment}")]
|
||||
FailedMigration { comment: String },
|
||||
|
||||
#[error(transparent)]
|
||||
Admin(#[from] AdminError),
|
||||
|
||||
#[error(transparent)]
|
||||
StdErr(#[from] cosmwasm_std::StdError),
|
||||
|
||||
#[error("{address} is already an authorised network monitor")]
|
||||
AlreadyAuthorised { address: Addr },
|
||||
|
||||
#[error("{address} is not an authorised network monitor")]
|
||||
NotAuthorised { address: Addr },
|
||||
|
||||
#[error("attempted to submit performance data for epoch {epoch_id} and node {node_id} whilst last submitted was {last_epoch_id} for node {last_node_id}")]
|
||||
StalePerformanceSubmission {
|
||||
epoch_id: EpochId,
|
||||
node_id: NodeId,
|
||||
last_epoch_id: EpochId,
|
||||
last_node_id: NodeId,
|
||||
},
|
||||
|
||||
#[error("the batch performance data has not been sorted")]
|
||||
UnsortedBatchSubmission,
|
||||
|
||||
#[error("node {node_id} does not appear to be bonded")]
|
||||
NodeNotBonded { node_id: NodeId },
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod constants;
|
||||
pub mod error;
|
||||
pub mod helpers;
|
||||
pub mod msg;
|
||||
pub mod types;
|
||||
|
||||
pub use error::*;
|
||||
pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
|
||||
pub use types::*;
|
||||
@@ -0,0 +1,121 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{EpochId, NodeId, NodePerformance};
|
||||
use cosmwasm_schema::cw_serde;
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
use crate::types::{
|
||||
EpochMeasurementsPagedResponse, EpochPerformancePagedResponse,
|
||||
FullHistoricalPerformancePagedResponse, NetworkMonitorResponse, NetworkMonitorsPagedResponse,
|
||||
NodeMeasurementsResponse, NodePerformancePagedResponse, NodePerformanceResponse,
|
||||
RetiredNetworkMonitorsPagedResponse,
|
||||
};
|
||||
|
||||
#[cw_serde]
|
||||
pub struct InstantiateMsg {
|
||||
pub mixnet_contract_address: String,
|
||||
pub authorised_network_monitors: Vec<String>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub enum ExecuteMsg {
|
||||
/// Change the admin
|
||||
UpdateAdmin { admin: String },
|
||||
|
||||
/// Attempt to submit performance data of a particular node for given epoch
|
||||
Submit {
|
||||
epoch: EpochId,
|
||||
data: NodePerformance,
|
||||
},
|
||||
|
||||
/// Attempt to submit performance data of a batch of nodes for given epoch
|
||||
BatchSubmit {
|
||||
epoch: EpochId,
|
||||
data: Vec<NodePerformance>,
|
||||
},
|
||||
|
||||
/// Attempt to authorise new network monitor for submitting performance data
|
||||
AuthoriseNetworkMonitor { address: String },
|
||||
|
||||
/// Attempt to retire an existing network monitor and forbid it from submitting any future performance data
|
||||
RetireNetworkMonitor { address: String },
|
||||
|
||||
/// An admin method to remove submitted node measurements. Used as an escape hatch should
|
||||
/// the data stored get too unwieldy.
|
||||
RemoveNodeMeasurements { epoch_id: EpochId, node_id: NodeId },
|
||||
|
||||
/// An admin method to remove submitted nodes measurements. Used as an escape hatch should
|
||||
/// the data stored get too unwieldy. Note: it is expected to get called multiple times
|
||||
/// until the response indicates all the epoch data has been removed.
|
||||
RemoveEpochMeasurements { epoch_id: EpochId },
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
#[cfg_attr(feature = "schema", derive(cosmwasm_schema::QueryResponses))]
|
||||
pub enum QueryMsg {
|
||||
#[cfg_attr(feature = "schema", returns(cw_controllers::AdminResponse))]
|
||||
Admin {},
|
||||
|
||||
/// Returns performance of particular node for the provided epoch
|
||||
#[cfg_attr(feature = "schema", returns(NodePerformanceResponse))]
|
||||
NodePerformance { epoch_id: EpochId, node_id: NodeId },
|
||||
|
||||
/// Returns historical performance for particular node
|
||||
#[cfg_attr(feature = "schema", returns(NodePerformancePagedResponse))]
|
||||
NodePerformancePaged {
|
||||
node_id: NodeId,
|
||||
start_after: Option<EpochId>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Returns all submitted measurements for the particular node
|
||||
#[cfg_attr(feature = "schema", returns(NodeMeasurementsResponse))]
|
||||
NodeMeasurements { epoch_id: EpochId, node_id: NodeId },
|
||||
|
||||
/// Returns (paged) measurements for particular epoch
|
||||
#[cfg_attr(feature = "schema", returns(EpochMeasurementsPagedResponse))]
|
||||
EpochMeasurementsPaged {
|
||||
epoch_id: EpochId,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Returns (paged) performance for particular epoch
|
||||
#[cfg_attr(feature = "schema", returns(EpochPerformancePagedResponse))]
|
||||
EpochPerformancePaged {
|
||||
epoch_id: EpochId,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Returns full (paged) historical performance of the whole network
|
||||
#[cfg_attr(feature = "schema", returns(FullHistoricalPerformancePagedResponse))]
|
||||
FullHistoricalPerformancePaged {
|
||||
start_after: Option<(EpochId, NodeId)>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Returns information about particular network monitor
|
||||
#[cfg_attr(feature = "schema", returns(NetworkMonitorResponse))]
|
||||
NetworkMonitor { address: String },
|
||||
|
||||
/// Returns information about all network monitors
|
||||
#[cfg_attr(feature = "schema", returns(NetworkMonitorsPagedResponse))]
|
||||
NetworkMonitorsPaged {
|
||||
start_after: Option<String>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Returns information about all retired network monitors
|
||||
#[cfg_attr(feature = "schema", returns(RetiredNetworkMonitorsPagedResponse))]
|
||||
RetiredNetworkMonitorsPaged {
|
||||
start_after: Option<String>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct MigrateMsg {
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use cosmwasm_std::{Addr, Env};
|
||||
use nym_contracts_common::Percent;
|
||||
|
||||
pub type EpochId = u32;
|
||||
pub type NodeId = u32;
|
||||
|
||||
#[cw_serde]
|
||||
pub struct NetworkMonitorDetails {
|
||||
pub address: Addr,
|
||||
pub authorised_by: Addr,
|
||||
pub authorised_at_height: u64,
|
||||
}
|
||||
|
||||
impl NetworkMonitorDetails {
|
||||
pub fn retire(self, env: &Env, sender: &Addr) -> RetiredNetworkMonitor {
|
||||
RetiredNetworkMonitor {
|
||||
details: self,
|
||||
retired_by: sender.clone(),
|
||||
retired_at_height: env.block.height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct RetiredNetworkMonitor {
|
||||
pub details: NetworkMonitorDetails,
|
||||
pub retired_by: Addr,
|
||||
pub retired_at_height: u64,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
#[derive(Copy)]
|
||||
pub struct NodePerformance {
|
||||
#[serde(rename = "n")]
|
||||
pub node_id: NodeId,
|
||||
|
||||
// note: value is rounded to 2 decimal places.
|
||||
#[serde(rename = "p")]
|
||||
pub performance: Percent,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct NetworkMonitorSubmissionMetadata {
|
||||
pub last_submitted_epoch_id: EpochId,
|
||||
pub last_submitted_node_id: NodeId,
|
||||
}
|
||||
|
||||
// the internal values are always sorted
|
||||
#[cw_serde]
|
||||
pub struct NodeResults(Vec<Percent>);
|
||||
|
||||
impl NodeResults {
|
||||
pub fn new(initial: Percent) -> NodeResults {
|
||||
NodeResults(vec![initial.round_to_two_decimal_places()])
|
||||
}
|
||||
|
||||
// ASSUMPTION: number of NM will be relatively small, so loading the whole vector of values
|
||||
// to insert new one and resave is cheap
|
||||
pub fn insert_new(&mut self, result: Percent) {
|
||||
let result = result.round_to_two_decimal_places();
|
||||
let pos = self.0.binary_search(&result).unwrap_or_else(|e| e);
|
||||
self.0.insert(pos, result);
|
||||
}
|
||||
|
||||
// SAFETY: there are no codepaths that allow constructing empty struct
|
||||
pub fn median(&self) -> Percent {
|
||||
let len = self.0.len();
|
||||
if len % 2 == 1 {
|
||||
// odd number of elements: return the middle one
|
||||
self.0[len / 2]
|
||||
} else {
|
||||
// even number: average the two middle elements
|
||||
let mid1 = self.0[len / 2 - 1];
|
||||
let mid2 = self.0[len / 2];
|
||||
mid1.average(&mid2).round_to_two_decimal_places()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &[Percent] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct NodePerformanceResponse {
|
||||
pub performance: Option<Percent>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct NodeMeasurementsResponse {
|
||||
pub measurements: Option<NodeResults>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
#[derive(Copy)]
|
||||
pub struct EpochNodePerformance {
|
||||
pub epoch: EpochId,
|
||||
pub performance: Option<Percent>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct NodePerformancePagedResponse {
|
||||
pub node_id: NodeId,
|
||||
pub performance: Vec<EpochNodePerformance>,
|
||||
pub start_next_after: Option<EpochId>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct EpochPerformancePagedResponse {
|
||||
pub epoch_id: EpochId,
|
||||
pub performance: Vec<NodePerformance>,
|
||||
pub start_next_after: Option<NodeId>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct NodeMeasurement {
|
||||
pub node_id: NodeId,
|
||||
pub measurements: NodeResults,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct EpochMeasurementsPagedResponse {
|
||||
pub epoch_id: EpochId,
|
||||
pub measurements: Vec<NodeMeasurement>,
|
||||
pub start_next_after: Option<NodeId>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
#[derive(Copy)]
|
||||
pub struct HistoricalPerformance {
|
||||
pub epoch_id: EpochId,
|
||||
pub node_id: NodeId,
|
||||
pub performance: Percent,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct FullHistoricalPerformancePagedResponse {
|
||||
pub performance: Vec<HistoricalPerformance>,
|
||||
pub start_next_after: Option<(EpochId, NodeId)>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct NetworkMonitorInformation {
|
||||
pub details: NetworkMonitorDetails,
|
||||
pub current_submission_metadata: NetworkMonitorSubmissionMetadata,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct NetworkMonitorResponse {
|
||||
pub info: Option<NetworkMonitorInformation>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct NetworkMonitorsPagedResponse {
|
||||
pub info: Vec<NetworkMonitorInformation>,
|
||||
pub start_next_after: Option<String>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct RetiredNetworkMonitorsPagedResponse {
|
||||
pub info: Vec<RetiredNetworkMonitor>,
|
||||
pub start_next_after: Option<String>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct RemoveEpochMeasurementsResponse {
|
||||
pub additional_entries_to_remove_remaining: bool,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
#[derive(Default)]
|
||||
pub struct BatchSubmissionResult {
|
||||
pub accepted_scores: u64,
|
||||
pub non_existent_nodes: Vec<NodeId>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn p(raw: impl AsRef<str>) -> Percent {
|
||||
raw.as_ref().parse().unwrap()
|
||||
}
|
||||
|
||||
fn ps(raw: &[&str]) -> Vec<Percent> {
|
||||
raw.iter().map(p).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_results_insertion() {
|
||||
let initial = NodeResults::new(p("0.5"));
|
||||
|
||||
let mut smaller = initial.clone();
|
||||
let mut greater = initial.clone();
|
||||
|
||||
smaller.insert_new(p("0.4"));
|
||||
greater.insert_new(p("0.6"));
|
||||
|
||||
assert_eq!(smaller.0, ps(&["0.4", "0.5"]));
|
||||
assert_eq!(greater.0, ps(&["0.5", "0.6"]));
|
||||
|
||||
let mut another = NodeResults(ps(&["0.1", "0.4", "0.5", "0.6", "0.6", "1.0"]));
|
||||
another.insert_new(p("0.6"));
|
||||
another.insert_new(p("0.2"));
|
||||
another.insert_new(p("0.7"));
|
||||
another.insert_new(p("0.3"));
|
||||
another.insert_new(p("0.3"));
|
||||
another.insert_new(p("0.55"));
|
||||
|
||||
assert_eq!(
|
||||
another.0,
|
||||
ps(&[
|
||||
"0.1", "0.2", "0.3", "0.3", "0.4", "0.5", "0.55", "0.6", "0.6", "0.6", "0.7", "1.0"
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_results_median() {
|
||||
let results = NodeResults(ps(&["0.1"]));
|
||||
assert_eq!(results.median(), p("0.1"));
|
||||
|
||||
let results = NodeResults(ps(&["0.1", "0.2"]));
|
||||
assert_eq!(results.median(), p("0.15"));
|
||||
|
||||
let results = NodeResults(ps(&["0.1", "0.2", "0.3"]));
|
||||
assert_eq!(results.median(), p("0.2"));
|
||||
|
||||
let results = NodeResults(ps(&["0.1", "0.2", "0.3", "0.4"]));
|
||||
assert_eq!(results.median(), p("0.25"));
|
||||
|
||||
let results = NodeResults(ps(&["0.1", "0.2", "0.3", "0.4", "0.5"]));
|
||||
assert_eq!(results.median(), p("0.3"));
|
||||
|
||||
let results = NodeResults(ps(&["0", "0", "1", "1", "1", "1", "1"]));
|
||||
assert_eq!(results.median(), p("1"));
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,8 @@ nym-credentials = { path = "../credentials" }
|
||||
nym-compact-ecash = { path = "../nym_offline_compact_ecash" }
|
||||
nym-ecash-time = { path = "../ecash-time" }
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx-pool-guard]
|
||||
path = "../../sqlx-pool-guard"
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
|
||||
workspace = true
|
||||
@@ -31,8 +33,13 @@ features = ["rt-multi-thread", "net", "signal", "fs"]
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
|
||||
sqlx = { workspace = true, features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
"macros",
|
||||
"migrate",
|
||||
] }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
[features]
|
||||
persistent-storage = ["bincode", "serde"]
|
||||
persistent-storage = ["bincode", "serde"]
|
||||
|
||||
@@ -7,10 +7,11 @@ use crate::models::{
|
||||
};
|
||||
use nym_ecash_time::Date;
|
||||
use sqlx::{Executor, Sqlite, Transaction};
|
||||
use sqlx_pool_guard::SqlitePoolGuard;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SqliteEcashTicketbookManager {
|
||||
connection_pool: sqlx::SqlitePool,
|
||||
connection_pool: SqlitePoolGuard,
|
||||
}
|
||||
|
||||
impl SqliteEcashTicketbookManager {
|
||||
@@ -19,7 +20,7 @@ impl SqliteEcashTicketbookManager {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `connection_pool`: database connection pool to use.
|
||||
pub fn new(connection_pool: sqlx::SqlitePool) -> Self {
|
||||
pub fn new(connection_pool: SqlitePoolGuard) -> Self {
|
||||
SqliteEcashTicketbookManager { connection_pool }
|
||||
}
|
||||
|
||||
@@ -33,7 +34,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"DELETE FROM ecash_ticketbook WHERE expiration_date <= ?",
|
||||
deadline
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -60,7 +61,7 @@ impl SqliteEcashTicketbookManager {
|
||||
data,
|
||||
expiration_date,
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
@@ -90,7 +91,7 @@ impl SqliteEcashTicketbookManager {
|
||||
epoch_id,
|
||||
total_tickets,
|
||||
used_tickets,
|
||||
).execute(&self.connection_pool).await?;
|
||||
).execute(&*self.connection_pool).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -105,7 +106,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"#,
|
||||
)
|
||||
.bind(data)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.fetch_optional(&*self.connection_pool)
|
||||
.await?
|
||||
.is_some();
|
||||
|
||||
@@ -121,7 +122,7 @@ impl SqliteEcashTicketbookManager {
|
||||
FROM ecash_ticketbook
|
||||
"#,
|
||||
)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.fetch_all(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -143,7 +144,7 @@ impl SqliteEcashTicketbookManager {
|
||||
ticketbook_id,
|
||||
expected_current_total_spent
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?
|
||||
.rows_affected();
|
||||
Ok(affected > 0)
|
||||
@@ -153,7 +154,7 @@ impl SqliteEcashTicketbookManager {
|
||||
&self,
|
||||
) -> Result<Vec<StoredPendingTicketbook>, sqlx::Error> {
|
||||
sqlx::query_as("SELECT * FROM pending_issuance")
|
||||
.fetch_all(&self.connection_pool)
|
||||
.fetch_all(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -165,7 +166,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"DELETE FROM pending_issuance WHERE deposit_id = ?",
|
||||
pending_id
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -182,7 +183,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"#,
|
||||
epoch_id
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.fetch_optional(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -208,7 +209,7 @@ impl SqliteEcashTicketbookManager {
|
||||
serialisation_revision,
|
||||
epoch_id
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -225,7 +226,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"#,
|
||||
epoch_id
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.fetch_optional(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -251,7 +252,7 @@ impl SqliteEcashTicketbookManager {
|
||||
serialisation_revision,
|
||||
epoch_id,
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -269,7 +270,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"#,
|
||||
expiration_date
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.fetch_optional(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -298,7 +299,7 @@ impl SqliteEcashTicketbookManager {
|
||||
serialisation_revision,
|
||||
expiration_date
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ use sqlx::{
|
||||
sqlite::{SqliteAutoVacuum, SqliteSynchronous},
|
||||
ConnectOptions,
|
||||
};
|
||||
use sqlx_pool_guard::SqlitePoolGuard;
|
||||
use std::path::Path;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
@@ -54,15 +55,15 @@ impl PersistentStorage {
|
||||
/// * `database_path`: path to the database.
|
||||
pub async fn init<P: AsRef<Path>>(database_path: P) -> Result<Self, StorageError> {
|
||||
debug!(
|
||||
"Attempting to connect to database {:?}",
|
||||
database_path.as_ref().as_os_str()
|
||||
"Attempting to connect to database {}",
|
||||
database_path.as_ref().display()
|
||||
);
|
||||
|
||||
let opts = sqlx::sqlite::SqliteConnectOptions::new()
|
||||
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
|
||||
.synchronous(SqliteSynchronous::Normal)
|
||||
.auto_vacuum(SqliteAutoVacuum::Incremental)
|
||||
.filename(database_path)
|
||||
.filename(&database_path)
|
||||
.create_if_missing(true)
|
||||
.disable_statement_logging();
|
||||
|
||||
@@ -74,13 +75,17 @@ impl PersistentStorage {
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await {
|
||||
let connection_pool =
|
||||
SqlitePoolGuard::new(database_path.as_ref().to_path_buf(), connection_pool);
|
||||
|
||||
if let Err(err) = sqlx::migrate!("./migrations").run(&*connection_pool).await {
|
||||
error!("Failed to perform migration on the SQLx database: {err}");
|
||||
connection_pool.close().await;
|
||||
return Err(err.into());
|
||||
}
|
||||
|
||||
Ok(PersistentStorage {
|
||||
storage_manager: SqliteEcashTicketbookManager::new(connection_pool.clone()),
|
||||
storage_manager: SqliteEcashTicketbookManager::new(connection_pool),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,8 @@ impl BandwidthStorageManager {
|
||||
debug!(available = available_bi2, required = required_bi2);
|
||||
|
||||
self.consume_bandwidth(required_bandwidth).await?;
|
||||
Ok(available_bandwidth)
|
||||
let remaining_bandwidth = self.client_bandwidth.available().await;
|
||||
Ok(remaining_bandwidth)
|
||||
}
|
||||
|
||||
async fn expire_bandwidth(&mut self) -> Result<()> {
|
||||
|
||||
@@ -14,6 +14,7 @@ use nym_dkg::bte::{
|
||||
};
|
||||
use nym_dkg::interpolation::polynomial::Polynomial;
|
||||
use nym_dkg::{combine_shares, Dealing, NodeIndex, Share, Threshold};
|
||||
use rand::CryptoRng;
|
||||
use rand_core::{RngCore, SeedableRng};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -31,7 +32,7 @@ pub fn precomputing_g2_generator_for_miller_loop(c: &mut Criterion) {
|
||||
}
|
||||
|
||||
fn prepare_keys(
|
||||
mut rng: impl RngCore,
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
nodes: usize,
|
||||
) -> (BTreeMap<NodeIndex, PublicKey>, Vec<DecryptionKey>) {
|
||||
let params = setup();
|
||||
@@ -50,7 +51,7 @@ fn prepare_keys(
|
||||
}
|
||||
|
||||
fn prepare_resharing(
|
||||
mut rng: impl RngCore,
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
params: &Params,
|
||||
nodes: usize,
|
||||
threshold: Threshold,
|
||||
@@ -68,7 +69,7 @@ fn prepare_resharing(
|
||||
for (i, ref mut dk) in dks.iter_mut().enumerate() {
|
||||
let shares = first_dealings
|
||||
.iter()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.map(|dealing| decrypt_share(params, dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
let recovered_secret =
|
||||
@@ -154,7 +155,9 @@ pub fn verifying_dealing_made_for_3_parties_and_recovering_share(c: &mut Criteri
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
assert!(dealing.verify(¶ms, threshold, &receivers, None).is_ok());
|
||||
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap());
|
||||
black_box(
|
||||
decrypt_share(¶ms, first_key, 0, &dealing.ciphertexts, None).unwrap(),
|
||||
);
|
||||
})
|
||||
},
|
||||
);
|
||||
@@ -237,7 +240,9 @@ pub fn verifying_dealing_made_for_20_parties_and_recovering_share(c: &mut Criter
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
assert!(dealing.verify(¶ms, threshold, &receivers, None).is_ok());
|
||||
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap());
|
||||
black_box(
|
||||
decrypt_share(¶ms, first_key, 0, &dealing.ciphertexts, None).unwrap(),
|
||||
);
|
||||
})
|
||||
},
|
||||
);
|
||||
@@ -320,7 +325,9 @@ pub fn verifying_dealing_made_for_100_parties_and_recovering_share(c: &mut Crite
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
assert!(dealing.verify(¶ms, threshold, &receivers, None).is_ok());
|
||||
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap());
|
||||
black_box(
|
||||
decrypt_share(¶ms, first_key, 0, &dealing.ciphertexts, None).unwrap(),
|
||||
);
|
||||
})
|
||||
},
|
||||
);
|
||||
@@ -547,7 +554,7 @@ pub fn share_decryption(c: &mut Criterion) {
|
||||
let (ciphertexts, _) = encrypt_shares(&[(&share, pk.public_key())], ¶ms, &mut rng);
|
||||
|
||||
c.bench_function("single share decryption", |b| {
|
||||
b.iter(|| black_box(decrypt_share(&dk, 0, &ciphertexts, None)))
|
||||
b.iter(|| black_box(decrypt_share(¶ms, &dk, 0, &ciphertexts, None)))
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::{Chunk, ChunkedShare, Share};
|
||||
use bls12_381::{G1Affine, G1Projective, G2Prepared, G2Projective, Gt, Scalar};
|
||||
use ff::Field;
|
||||
use group::{Curve, Group, GroupEncoding};
|
||||
use rand::CryptoRng;
|
||||
use rand_core::RngCore;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Neg;
|
||||
@@ -191,7 +192,7 @@ impl HazmatRandomness {
|
||||
pub fn encrypt_shares(
|
||||
shares: &[(&Share, &PublicKey)],
|
||||
params: &Params,
|
||||
mut rng: impl RngCore,
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
) -> (Ciphertexts, HazmatRandomness) {
|
||||
let g1 = G1Projective::generator();
|
||||
|
||||
@@ -262,6 +263,7 @@ pub fn encrypt_shares(
|
||||
}
|
||||
|
||||
pub fn decrypt_share(
|
||||
params: &Params,
|
||||
dk: &DecryptionKey,
|
||||
// in the case of multiple receivers, specifies which index of ciphertext chunks should be used
|
||||
i: usize,
|
||||
@@ -270,6 +272,10 @@ pub fn decrypt_share(
|
||||
) -> Result<Share, DkgError> {
|
||||
let mut plaintext = ChunkedShare::default();
|
||||
|
||||
if !ciphertext.verify_integrity(params) {
|
||||
return Err(DkgError::FailedCiphertextIntegrityCheck);
|
||||
}
|
||||
|
||||
if i >= ciphertext.ciphertext_chunks.len() {
|
||||
return Err(DkgError::UnavailableCiphertext(i));
|
||||
}
|
||||
@@ -461,10 +467,22 @@ mod tests {
|
||||
let (ciphertext, hazmat) = encrypt_shares(shares, ¶ms, &mut rng);
|
||||
verify_hazmat_rand(&ciphertext, &hazmat);
|
||||
|
||||
let recovered1 =
|
||||
decrypt_share(&decryption_key1, 0, &ciphertext, Some(lookup_table)).unwrap();
|
||||
let recovered2 =
|
||||
decrypt_share(&decryption_key2, 1, &ciphertext, Some(lookup_table)).unwrap();
|
||||
let recovered1 = decrypt_share(
|
||||
¶ms,
|
||||
&decryption_key1,
|
||||
0,
|
||||
&ciphertext,
|
||||
Some(lookup_table),
|
||||
)
|
||||
.unwrap();
|
||||
let recovered2 = decrypt_share(
|
||||
¶ms,
|
||||
&decryption_key2,
|
||||
1,
|
||||
&ciphertext,
|
||||
Some(lookup_table),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(m1, recovered1);
|
||||
assert_eq!(m2, recovered2);
|
||||
}
|
||||
@@ -490,10 +508,22 @@ mod tests {
|
||||
let (ciphertext, hazmat) = encrypt_shares(shares, ¶ms, &mut rng);
|
||||
verify_hazmat_rand(&ciphertext, &hazmat);
|
||||
|
||||
let recovered1 =
|
||||
decrypt_share(&decryption_key1, 0, &ciphertext, Some(lookup_table)).unwrap();
|
||||
let recovered2 =
|
||||
decrypt_share(&decryption_key2, 1, &ciphertext, Some(lookup_table)).unwrap();
|
||||
let recovered1 = decrypt_share(
|
||||
¶ms,
|
||||
&decryption_key1,
|
||||
0,
|
||||
&ciphertext,
|
||||
Some(lookup_table),
|
||||
)
|
||||
.unwrap();
|
||||
let recovered2 = decrypt_share(
|
||||
¶ms,
|
||||
&decryption_key2,
|
||||
1,
|
||||
&ciphertext,
|
||||
Some(lookup_table),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(m1, recovered1);
|
||||
assert_eq!(m2, recovered2);
|
||||
}
|
||||
@@ -574,7 +604,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn ciphertexts_roundtrip() {
|
||||
fn random_ciphertexts(mut rng: impl RngCore, num_receivers: usize) -> Ciphertexts {
|
||||
fn random_ciphertexts(
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
num_receivers: usize,
|
||||
) -> Ciphertexts {
|
||||
Ciphertexts {
|
||||
rr: (0..NUM_CHUNKS)
|
||||
.map(|_| G1Projective::random(&mut rng))
|
||||
|
||||
@@ -9,11 +9,15 @@ use bls12_381::{G1Projective, G2Projective, Scalar};
|
||||
use ff::Field;
|
||||
use group::GroupEncoding;
|
||||
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
|
||||
use rand::CryptoRng;
|
||||
use rand_core::RngCore;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
// produces public key and a decryption key for the root of the tree
|
||||
pub fn keygen(params: &Params, mut rng: impl RngCore) -> (DecryptionKey, PublicKeyWithProof) {
|
||||
pub fn keygen(
|
||||
params: &Params,
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
) -> (DecryptionKey, PublicKeyWithProof) {
|
||||
let g1 = G1Projective::generator();
|
||||
let g2 = G2Projective::generator();
|
||||
|
||||
@@ -244,7 +248,7 @@ pub struct KeyPair {
|
||||
}
|
||||
|
||||
impl KeyPair {
|
||||
pub fn new(params: &Params, rng: impl RngCore) -> Self {
|
||||
pub fn new(params: &Params, rng: impl RngCore + CryptoRng) -> Self {
|
||||
let (dk, pk) = keygen(params, rng);
|
||||
Self {
|
||||
private_key: dk,
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::utils::{deserialize_scalar, RandomOracleBuilder};
|
||||
use bls12_381::{G1Projective, Scalar};
|
||||
use ff::Field;
|
||||
use group::{Group, GroupEncoding};
|
||||
use rand::Rng;
|
||||
use rand::{CryptoRng, Rng};
|
||||
use rand_core::{RngCore, SeedableRng};
|
||||
|
||||
const CHUNKING_ORACLE_DOMAIN: &[u8] =
|
||||
@@ -28,6 +28,7 @@ const SECURITY_PARAMETER: usize = 256;
|
||||
|
||||
/// ceil(SECURITY_PARAMETER / PARALLEL_RUNS) in the paper
|
||||
const NUM_CHALLENGE_BITS: usize = SECURITY_PARAMETER.div_ceil(PARALLEL_RUNS);
|
||||
const EE: usize = 1 << NUM_CHALLENGE_BITS;
|
||||
|
||||
// type alias for ease of use
|
||||
type FirstChallenge = Vec<Vec<Vec<u64>>>;
|
||||
@@ -94,7 +95,7 @@ impl ProofOfChunking {
|
||||
// Scalar(-1) would in reality be Scalar(q - 1), which is greater than Scalar(1) and opposite to
|
||||
// what we wanted.
|
||||
pub fn construct(
|
||||
mut rng: impl RngCore,
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
instance: Instance,
|
||||
witness_r: &[Scalar; NUM_CHUNKS],
|
||||
witnesses_s: &[Share],
|
||||
@@ -110,21 +111,20 @@ impl ProofOfChunking {
|
||||
// define bounds for the blinding factors
|
||||
let n = instance.public_keys.len();
|
||||
let m = NUM_CHUNKS;
|
||||
let ee = 1 << NUM_CHALLENGE_BITS;
|
||||
|
||||
// CHUNK_MAX corresponds to paper's B
|
||||
let ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) as u64;
|
||||
let zz = (2 * (PARALLEL_RUNS as u64))
|
||||
.checked_mul(ss)
|
||||
.expect("overflow in Z = 2 * l * S");
|
||||
// ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1))
|
||||
// Z = 2 * l * S
|
||||
let (ss, zz): (u64, u64) = compute_ss_zz(n, m)?;
|
||||
|
||||
let ss_scalar = Scalar::from(ss);
|
||||
|
||||
// rather than generating blinding factors in [-S, Z-1] directly,
|
||||
// do it via [0, Z - 1 + S + 1] and deal with the shift later.
|
||||
let combined_upper_range = (zz - 1)
|
||||
.checked_add(ss + 1)
|
||||
.expect("overflow in Z - 1 + S + 1");
|
||||
// combined_upper_range = Z - 1 + S + 1
|
||||
|
||||
let combined_upper_range = zz.checked_add(ss).ok_or(DkgError::ArithmeticOverflow {
|
||||
info: "ProofOfChunking::construct | Z - 1 + S + 1",
|
||||
})?;
|
||||
|
||||
let mut betas = Vec::with_capacity(PARALLEL_RUNS);
|
||||
let mut bs = Vec::with_capacity(PARALLEL_RUNS);
|
||||
@@ -178,12 +178,23 @@ impl ProofOfChunking {
|
||||
// I think this part is more readable with a range loop
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
for l in 0..PARALLEL_RUNS {
|
||||
let mut sum = 0;
|
||||
let mut sum: u64 = 0;
|
||||
|
||||
for (i, witness_i) in witnesses_s.iter().enumerate() {
|
||||
for (j, witness_ij) in witness_i.to_chunks().chunks.iter().enumerate() {
|
||||
debug_assert!(std::mem::size_of::<Chunk>() <= std::mem::size_of::<u64>());
|
||||
sum += first_challenge[i][j][l] * (*witness_ij as u64)
|
||||
// sum += first_challenge[i][j][l] * (*witness_ij as u64)
|
||||
sum = sum
|
||||
.checked_add(
|
||||
first_challenge[i][j][l]
|
||||
.checked_mul(*witness_ij as u64)
|
||||
.ok_or(DkgError::ArithmeticOverflow {
|
||||
info: "ProofOfChunking::construct | first_challenge[i][j][l] * witness_ij",
|
||||
})?,
|
||||
)
|
||||
.ok_or(DkgError::ArithmeticOverflow {
|
||||
info: "ProofOfChunking::construct | sum + (first_challenge[i][j][l] * witness_ij)",
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +202,18 @@ impl ProofOfChunking {
|
||||
continue 'retry_loop;
|
||||
}
|
||||
// shifted_blinding_factors[l] - ss restores it to "proper" [-S, Z - 1] range
|
||||
let response = sum + shifted_blinding_factors[l] - ss;
|
||||
// let response = sum + shifted_blinding_factors[l] - ss;
|
||||
let response = sum
|
||||
.checked_add(shifted_blinding_factors[l])
|
||||
.ok_or(DkgError::ArithmeticOverflow {
|
||||
info:
|
||||
"ProofOfChunking::construct | sum + (shifted_blinding_factors[l] - ss)",
|
||||
})?
|
||||
.checked_sub(ss)
|
||||
.ok_or(DkgError::ArithmeticUnderflow {
|
||||
info: "ProofOfChunking::construct | shifted_blinding_factors[l] - ss",
|
||||
})?;
|
||||
|
||||
if response < zz {
|
||||
responses_chunks.push(response)
|
||||
} else {
|
||||
@@ -276,11 +298,13 @@ impl ProofOfChunking {
|
||||
ensure_len!(&self.responses_r, n);
|
||||
ensure_len!(&self.responses_chunks, PARALLEL_RUNS);
|
||||
|
||||
let ee = 1 << NUM_CHALLENGE_BITS;
|
||||
// ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1))
|
||||
// Z = 2 * l * S
|
||||
|
||||
// CHUNK_MAX corresponds to paper's B
|
||||
let ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) as u64;
|
||||
let zz = 2 * (PARALLEL_RUNS as u64) * ss;
|
||||
let zz: u64 = match compute_ss_zz(n, m) {
|
||||
Ok((_, zz_res)) => zz_res,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
for response_chunk in &self.responses_chunks {
|
||||
if response_chunk >= &zz {
|
||||
@@ -411,7 +435,7 @@ impl ProofOfChunking {
|
||||
random_oracle_builder.update(lambda_e.to_be_bytes());
|
||||
|
||||
let mut oracle = rand_chacha::ChaCha20Rng::from_seed(random_oracle_builder.finalize());
|
||||
let range_max_excl = 1 << NUM_CHALLENGE_BITS;
|
||||
let range_max_excl = EE as u64;
|
||||
|
||||
(0..n)
|
||||
.map(|_| {
|
||||
@@ -637,6 +661,50 @@ impl ProofOfChunking {
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_ss_zz(n: usize, m: usize) -> Result<(u64, u64), DkgError> {
|
||||
// let ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) as u64;
|
||||
// CHUNK_MAX corresponds to paper's B
|
||||
|
||||
let ee = EE;
|
||||
|
||||
let ss = n
|
||||
.checked_mul(m)
|
||||
.ok_or(DkgError::ArithmeticOverflow {
|
||||
info: "ProofOfChunking::compute_ss_zz | n * m",
|
||||
})?
|
||||
.checked_mul(
|
||||
CHUNK_SIZE
|
||||
.checked_sub(1)
|
||||
.ok_or(DkgError::ArithmeticUnderflow {
|
||||
info: "ProofOfChunking::compute_ss_zz | (CHUNK_SIZE - 1)",
|
||||
})?
|
||||
.checked_mul(ee.checked_sub(1).ok_or(DkgError::ArithmeticUnderflow {
|
||||
info: "ProofOfChunking::compute_ss_zz | (ee - 1)",
|
||||
})?)
|
||||
.ok_or(DkgError::ArithmeticOverflow {
|
||||
info: "ProofOfChunking::compute_ss_zz | (CHUNK_SIZE - 1) * (ee - 1)",
|
||||
})?,
|
||||
)
|
||||
.ok_or(DkgError::ArithmeticOverflow {
|
||||
info: "ProofOfChunking::compute_ss_zz | ss_lhs * ss_rhs",
|
||||
})? as u64;
|
||||
|
||||
// let zz = 2 * PARALLEL_RUNS as u64 * ss;
|
||||
// Z = 2 * l * S
|
||||
|
||||
let zz = 2u64
|
||||
.checked_mul(PARALLEL_RUNS as u64)
|
||||
.ok_or(DkgError::ArithmeticOverflow {
|
||||
info: "ProofOfChunking::compute_ss_zz | 2 * l",
|
||||
})?
|
||||
.checked_mul(ss)
|
||||
.ok_or(DkgError::ArithmeticOverflow {
|
||||
info: "ProofOfChunking::compute_ss_zz | (2 * l) * S",
|
||||
})?;
|
||||
|
||||
Ok((ss, zz))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -652,7 +720,9 @@ mod tests {
|
||||
ciphertext_chunks: Vec<[G1Projective; NUM_CHUNKS]>,
|
||||
}
|
||||
|
||||
fn setup(mut rng: impl RngCore) -> (OwnedInstance, [Scalar; NUM_CHUNKS], Vec<Share>) {
|
||||
fn setup(
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
) -> (OwnedInstance, [Scalar; NUM_CHUNKS], Vec<Share>) {
|
||||
let g1 = G1Projective::generator();
|
||||
|
||||
let mut pks = Vec::with_capacity(NODES);
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::utils::hash_to_scalar;
|
||||
use bls12_381::{G1Projective, Scalar};
|
||||
use ff::Field;
|
||||
use group::GroupEncoding;
|
||||
use rand::CryptoRng;
|
||||
use rand_core::RngCore;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
@@ -20,7 +21,11 @@ pub struct ProofOfDiscreteLog {
|
||||
}
|
||||
|
||||
impl ProofOfDiscreteLog {
|
||||
pub fn construct(mut rng: impl RngCore, public: &G1Projective, witness: &Scalar) -> Self {
|
||||
pub fn construct(
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
public: &G1Projective,
|
||||
witness: &Scalar,
|
||||
) -> Self {
|
||||
let mut rand_x = Scalar::random(&mut rng);
|
||||
let rand_commitment = G1Projective::generator() * rand_x;
|
||||
let challenge = Self::compute_challenge(public, &rand_commitment);
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::{NodeIndex, Share};
|
||||
use bls12_381::{G1Projective, G2Projective, Scalar};
|
||||
use ff::Field;
|
||||
use group::GroupEncoding;
|
||||
use rand::CryptoRng;
|
||||
use rand_core::RngCore;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -87,7 +88,7 @@ pub struct ProofOfSecretSharing {
|
||||
|
||||
impl ProofOfSecretSharing {
|
||||
pub fn construct(
|
||||
mut rng: impl RngCore,
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
instance: Instance,
|
||||
witness_r: &Scalar,
|
||||
witnesses_s: &[Share],
|
||||
@@ -309,13 +310,14 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::interpolation::polynomial::Polynomial;
|
||||
use group::Group;
|
||||
use rand::CryptoRng;
|
||||
use rand_core::SeedableRng;
|
||||
|
||||
const NODES: u64 = 50;
|
||||
const THRESHOLD: u64 = 40;
|
||||
|
||||
fn setup(
|
||||
mut rng: impl RngCore,
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
) -> (
|
||||
BTreeMap<NodeIndex, PublicKey>,
|
||||
PublicCoefficients,
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::utils::deserialize_g2;
|
||||
use crate::{NodeIndex, Share, Threshold};
|
||||
use bls12_381::{G2Projective, Scalar};
|
||||
use group::GroupEncoding;
|
||||
use rand::CryptoRng;
|
||||
use rand_core::RngCore;
|
||||
use std::collections::BTreeMap;
|
||||
use zeroize::Zeroize;
|
||||
@@ -94,7 +95,7 @@ impl Dealing {
|
||||
// I'm not a big fan of this function signature, but I'm not clear on how to improve it while
|
||||
// allowing the dealer to skip decryption of its own share if it was also one of the receivers
|
||||
pub fn create(
|
||||
mut rng: impl RngCore,
|
||||
mut rng: impl RngCore + CryptoRng + CryptoRng,
|
||||
params: &Params,
|
||||
dealer_index: NodeIndex,
|
||||
threshold: Threshold,
|
||||
@@ -484,7 +485,7 @@ mod tests {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = dealings
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
derived_secrets.push(
|
||||
combine_shares(shares, &receivers.keys().copied().collect::<Vec<_>>()).unwrap(),
|
||||
@@ -593,7 +594,7 @@ mod tests {
|
||||
for (i, (dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = dealings
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
let recovered_secret = combine_shares(shares, &dealer_indices).unwrap();
|
||||
|
||||
@@ -99,6 +99,12 @@ pub enum DkgError {
|
||||
"The reshared dealing has different public constant coefficient than its prior variant"
|
||||
)]
|
||||
InvalidResharing,
|
||||
|
||||
#[error("Arithmetic Overflow: {info}")]
|
||||
ArithmeticOverflow { info: &'static str },
|
||||
|
||||
#[error("Arithmetic Underflow: {info}")]
|
||||
ArithmeticUnderflow { info: &'static str },
|
||||
}
|
||||
|
||||
impl DkgError {
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::utils::deserialize_g2;
|
||||
use bls12_381::{G2Projective, Scalar};
|
||||
use ff::Field;
|
||||
use group::GroupEncoding;
|
||||
use rand::CryptoRng;
|
||||
use rand_core::RngCore;
|
||||
use std::ops::{Add, Index, IndexMut};
|
||||
use zeroize::Zeroize;
|
||||
@@ -120,7 +121,7 @@ impl Polynomial {
|
||||
// for polynomial of degree n, we generate n+1 values
|
||||
// (for example for degree 1, like y = x + 2, we need [2,1])
|
||||
/// Creates new pseudorandom polynomial of specified degree.
|
||||
pub fn new_random(mut rng: impl RngCore, degree: u64) -> Self {
|
||||
pub fn new_random(mut rng: impl RngCore + CryptoRng + CryptoRng, degree: u64) -> Self {
|
||||
Polynomial {
|
||||
coefficients: (0..=degree).map(|_| Scalar::random(&mut rng)).collect(),
|
||||
}
|
||||
|
||||
@@ -53,11 +53,12 @@ fn single_sender() {
|
||||
|
||||
// make sure each share is actually decryptable (even though proofs say they must be, perform this sanity check)
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let _recovered = decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap();
|
||||
let _recovered = decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap();
|
||||
}
|
||||
|
||||
// and for good measure, check that the dealer's share matches decryption result
|
||||
let recovered_dealer = decrypt_share(&full_keys[0].0, 0, &dealing.ciphertexts, None).unwrap();
|
||||
let recovered_dealer =
|
||||
decrypt_share(¶ms, &full_keys[0].0, 0, &dealing.ciphertexts, None).unwrap();
|
||||
assert_eq!(recovered_dealer, dealer_share.unwrap());
|
||||
}
|
||||
|
||||
@@ -115,7 +116,7 @@ fn full_threshold_secret_sharing() {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = dealings
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
// we know dealer_share matches, but it would be inconvenient to try to put them in here,
|
||||
@@ -189,7 +190,7 @@ fn full_threshold_secret_resharing() {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = first_dealings
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
let recovered_secret =
|
||||
@@ -240,7 +241,7 @@ fn full_threshold_secret_resharing() {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = resharing_dealings
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
let recovered_secret =
|
||||
@@ -305,7 +306,7 @@ fn full_threshold_secret_resharing_left_party() {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = first_dealings
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
let recovered_secret =
|
||||
@@ -369,7 +370,7 @@ fn full_threshold_secret_resharing_left_party() {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = resharing_dealings
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
let recovered_secret = combine_shares(shares, &node_indices).unwrap();
|
||||
|
||||
@@ -33,8 +33,8 @@ impl PersistentStatsStorage {
|
||||
/// * `database_path`: path to the database.
|
||||
pub async fn init<P: AsRef<Path> + Send>(database_path: P) -> Result<Self, StatsStorageError> {
|
||||
debug!(
|
||||
"Attempting to connect to database {:?}",
|
||||
database_path.as_ref().as_os_str()
|
||||
"Attempting to connect to database {}",
|
||||
database_path.as_ref().display()
|
||||
);
|
||||
|
||||
// TODO: we can inject here more stuff based on our gateway global config
|
||||
|
||||
@@ -82,8 +82,8 @@ impl GatewayStorage {
|
||||
message_retrieval_limit: i64,
|
||||
) -> Result<Self, GatewayStorageError> {
|
||||
debug!(
|
||||
"Attempting to connect to database {:?}",
|
||||
database_path.as_ref().as_os_str()
|
||||
"Attempting to connect to database {}",
|
||||
database_path.as_ref().display()
|
||||
);
|
||||
|
||||
// TODO: we can inject here more stuff based on our gateway global config
|
||||
|
||||
@@ -23,7 +23,6 @@ fn main() {
|
||||
"REWARDING_VALIDATOR_ADDRESS",
|
||||
"NYM_API",
|
||||
"NYXD_WS",
|
||||
"EXPLORER_API",
|
||||
"NYM_VPN_API",
|
||||
];
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#[cfg(feature = "network")]
|
||||
use crate::{DenomDetails, ValidatorDetails};
|
||||
use crate::{ApiUrlConst, DenomDetails, ValidatorDetails};
|
||||
|
||||
pub const NETWORK_NAME: &str = "mainnet";
|
||||
|
||||
@@ -17,6 +17,11 @@ pub const MIXNET_CONTRACT_ADDRESS: &str =
|
||||
"n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr";
|
||||
pub const VESTING_CONTRACT_ADDRESS: &str =
|
||||
"n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw";
|
||||
|
||||
// \/ TODO: this has to be updated once the contract is deployed
|
||||
pub const PERFORMANCE_CONTRACT_ADDRESS: &str = "";
|
||||
// /\ TODO: this has to be updated once the contract is deployed
|
||||
|
||||
pub const ECASH_CONTRACT_ADDRESS: &str =
|
||||
"n1r7s6aksyc6pqardx88k3rkgfagwvj4z4zum9mmz2sfk3zm2mha0sd4dnun";
|
||||
pub const GROUP_CONTRACT_ADDRESS: &str =
|
||||
@@ -29,10 +34,37 @@ pub const COCONUT_DKG_CONTRACT_ADDRESS: &str =
|
||||
pub const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy";
|
||||
|
||||
pub const NYXD_URL: &str = "https://rpc.nymtech.net";
|
||||
pub const NYM_API: &str = "https://validator.nymtech.net/api/";
|
||||
pub const NYXD_WS: &str = "wss://rpc.nymtech.net/websocket";
|
||||
pub const EXPLORER_API: &str = "https://explorer.nymtech.net/api/";
|
||||
|
||||
pub const NYM_API: &str = "https://validator.nymtech.net/api/";
|
||||
#[cfg(feature = "network")]
|
||||
pub const NYM_APIS: &[ApiUrlConst] = &[
|
||||
ApiUrlConst {
|
||||
url: NYM_API,
|
||||
front_hosts: None,
|
||||
},
|
||||
ApiUrlConst {
|
||||
url: "https://nym-fronntdoor.vercel.app/api/",
|
||||
front_hosts: Some(&["vercel.app", "vercel.com"]),
|
||||
},
|
||||
ApiUrlConst {
|
||||
url: "https://nym-frontdoor.global.ssl.fastly.net/api/",
|
||||
front_hosts: Some(&["yelp.global.ssl.fastly.net"]),
|
||||
},
|
||||
];
|
||||
|
||||
pub const NYM_VPN_API: &str = "https://nymvpn.com/api/";
|
||||
#[cfg(feature = "network")]
|
||||
pub const NYM_VPN_APIS: &[ApiUrlConst] = &[
|
||||
ApiUrlConst {
|
||||
url: NYM_VPN_API,
|
||||
front_hosts: Some(&["vercel.app", "vercel.com"]),
|
||||
},
|
||||
ApiUrlConst {
|
||||
url: "https://nymvpn-frontdoor.global.ssl.fastly.net/api/",
|
||||
front_hosts: Some(&["yelp.global.ssl.fastly.net"]),
|
||||
},
|
||||
];
|
||||
|
||||
// I'm making clippy mad on purpose, because that url HAS TO be updated and deployed before merging
|
||||
pub const EXIT_POLICY_URL: &str =
|
||||
@@ -123,7 +155,6 @@ pub fn export_to_env() {
|
||||
set_var_to_default(var_names::NYXD, NYXD_URL);
|
||||
set_var_to_default(var_names::NYM_API, NYM_API);
|
||||
set_var_to_default(var_names::NYXD_WEBSOCKET, NYXD_WS);
|
||||
set_var_to_default(var_names::EXPLORER_API, EXPLORER_API);
|
||||
set_var_to_default(var_names::EXIT_POLICY_URL, EXIT_POLICY_URL);
|
||||
set_var_to_default(var_names::NYM_VPN_API, NYM_VPN_API);
|
||||
}
|
||||
@@ -165,6 +196,5 @@ pub fn export_to_env_if_not_set() {
|
||||
set_var_conditionally_to_default(var_names::NYXD, NYXD_URL);
|
||||
set_var_conditionally_to_default(var_names::NYM_API, NYM_API);
|
||||
set_var_conditionally_to_default(var_names::NYXD_WEBSOCKET, NYXD_WS);
|
||||
set_var_conditionally_to_default(var_names::EXPLORER_API, EXPLORER_API);
|
||||
set_var_conditionally_to_default(var_names::EXIT_POLICY_URL, EXIT_POLICY_URL);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ pub struct ChainDetails {
|
||||
pub struct NymContracts {
|
||||
pub mixnet_contract_address: Option<String>,
|
||||
pub vesting_contract_address: Option<String>,
|
||||
#[serde(default)]
|
||||
pub performance_contract_address: Option<String>,
|
||||
pub ecash_contract_address: Option<String>,
|
||||
pub group_contract_address: Option<String>,
|
||||
pub multisig_contract_address: Option<String>,
|
||||
@@ -35,8 +37,38 @@ pub struct NymNetworkDetails {
|
||||
pub chain_details: ChainDetails,
|
||||
pub endpoints: Vec<ValidatorDetails>,
|
||||
pub contracts: NymContracts,
|
||||
pub explorer_api: Option<String>,
|
||||
pub nym_vpn_api_url: Option<String>,
|
||||
pub nym_api_urls: Option<Vec<ApiUrl>>,
|
||||
pub nym_vpn_api_urls: Option<Vec<ApiUrl>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
pub struct ApiUrl {
|
||||
/// Expects a string formatted Url
|
||||
///
|
||||
/// see https://docs.rs/url/latest/url/struct.Url.html
|
||||
pub url: String,
|
||||
/// Optional alternative equivalent hostnames. Each entry must parse as valid Host
|
||||
///
|
||||
/// see https://docs.rs/url/latest/url/enum.Host.html
|
||||
pub front_hosts: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
pub struct ApiUrlConst<'a> {
|
||||
pub url: &'a str,
|
||||
pub front_hosts: Option<&'a [&'a str]>,
|
||||
}
|
||||
|
||||
impl From<ApiUrlConst<'_>> for ApiUrl {
|
||||
fn from(value: ApiUrlConst) -> Self {
|
||||
ApiUrl {
|
||||
url: value.url.to_string(),
|
||||
front_hosts: value
|
||||
.front_hosts
|
||||
.map(|slice| slice.iter().map(|s| s.to_string()).collect()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// by default we assume the same defaults as mainnet, i.e. same prefixes and denoms
|
||||
@@ -65,8 +97,9 @@ impl NymNetworkDetails {
|
||||
},
|
||||
endpoints: Default::default(),
|
||||
contracts: Default::default(),
|
||||
explorer_api: Default::default(),
|
||||
nym_vpn_api_url: Default::default(),
|
||||
nym_api_urls: Default::default(),
|
||||
nym_vpn_api_urls: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +157,6 @@ impl NymNetworkDetails {
|
||||
.with_group_contract(get_optional_env(var_names::GROUP_CONTRACT_ADDRESS))
|
||||
.with_multisig_contract(get_optional_env(var_names::MULTISIG_CONTRACT_ADDRESS))
|
||||
.with_coconut_dkg_contract(get_optional_env(var_names::COCONUT_DKG_CONTRACT_ADDRESS))
|
||||
.with_explorer_api(get_optional_env(var_names::EXPLORER_API))
|
||||
.with_nym_vpn_api_url(get_optional_env(var_names::NYM_VPN_API))
|
||||
}
|
||||
|
||||
@@ -145,6 +177,9 @@ impl NymNetworkDetails {
|
||||
contracts: NymContracts {
|
||||
mixnet_contract_address: parse_optional_str(mainnet::MIXNET_CONTRACT_ADDRESS),
|
||||
vesting_contract_address: parse_optional_str(mainnet::VESTING_CONTRACT_ADDRESS),
|
||||
performance_contract_address: parse_optional_str(
|
||||
mainnet::PERFORMANCE_CONTRACT_ADDRESS,
|
||||
),
|
||||
ecash_contract_address: parse_optional_str(mainnet::ECASH_CONTRACT_ADDRESS),
|
||||
group_contract_address: parse_optional_str(mainnet::GROUP_CONTRACT_ADDRESS),
|
||||
multisig_contract_address: parse_optional_str(mainnet::MULTISIG_CONTRACT_ADDRESS),
|
||||
@@ -152,8 +187,9 @@ impl NymNetworkDetails {
|
||||
mainnet::COCONUT_DKG_CONTRACT_ADDRESS,
|
||||
),
|
||||
},
|
||||
explorer_api: parse_optional_str(mainnet::EXPLORER_API),
|
||||
nym_vpn_api_url: parse_optional_str(mainnet::NYM_VPN_API),
|
||||
nym_api_urls: None,
|
||||
nym_vpn_api_urls: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,7 +229,6 @@ impl NymNetworkDetails {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -297,12 +332,6 @@ impl NymNetworkDetails {
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_explorer_api<S: Into<String>>(mut self, endpoint: Option<S>) -> Self {
|
||||
self.explorer_api = endpoint.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_nym_vpn_api_url<S: Into<String>>(mut self, endpoint: Option<S>) -> Self {
|
||||
self.nym_vpn_api_url = endpoint.map(Into::into);
|
||||
|
||||
@@ -22,7 +22,6 @@ pub const REWARDING_VALIDATOR_ADDRESS: &str = "REWARDING_VALIDATOR_ADDRESS";
|
||||
pub const NYXD: &str = "NYXD";
|
||||
pub const NYM_API: &str = "NYM_API";
|
||||
pub const NYXD_WEBSOCKET: &str = "NYXD_WS";
|
||||
pub const EXPLORER_API: &str = "EXPLORER_API";
|
||||
pub const EXIT_POLICY_URL: &str = "EXIT_POLICY";
|
||||
pub const NYM_VPN_API: &str = "NYM_VPN_API";
|
||||
pub const CLIENT_STATS_COLLECTION_PROVIDER: &str = "CLIENT_STATS_COLLECTION_PROVIDER";
|
||||
|
||||
@@ -31,9 +31,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.97"
|
||||
version = "1.0.98"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
|
||||
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||
|
||||
[[package]]
|
||||
name = "ark-bls12-381"
|
||||
@@ -1133,6 +1133,19 @@ dependencies = [
|
||||
"vergen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-contracts-common-testing"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cosmwasm-std",
|
||||
"cw-multi-test",
|
||||
"cw-storage-plus",
|
||||
"rand",
|
||||
"rand_chacha",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-crypto"
|
||||
version = "0.4.0"
|
||||
@@ -1214,7 +1227,9 @@ dependencies = [
|
||||
"cw2",
|
||||
"easy-addr",
|
||||
"nym-contracts-common",
|
||||
"nym-contracts-common-testing",
|
||||
"nym-crypto",
|
||||
"nym-mixnet-contract",
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-vesting-contract-common",
|
||||
"rand",
|
||||
@@ -1238,7 +1253,6 @@ dependencies = [
|
||||
"schemars",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde-json-wasm",
|
||||
"serde_repr",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
@@ -1276,6 +1290,38 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-performance-contract"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-controllers",
|
||||
"cw-storage-plus",
|
||||
"cw2",
|
||||
"nym-contracts-common",
|
||||
"nym-contracts-common-testing",
|
||||
"nym-crypto",
|
||||
"nym-mixnet-contract",
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-performance-contract-common",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-performance-contract-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-controllers",
|
||||
"nym-contracts-common",
|
||||
"schemars",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-pool-contract"
|
||||
version = "0.1.0"
|
||||
@@ -1284,14 +1330,11 @@ dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-controllers",
|
||||
"cw-multi-test",
|
||||
"cw-storage-plus",
|
||||
"cw2",
|
||||
"nym-contracts-common",
|
||||
"nym-contracts-common-testing",
|
||||
"nym-pool-contract-common",
|
||||
"rand",
|
||||
"rand_chacha",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -9,6 +9,7 @@ members = [
|
||||
"multisig/cw3-flex-multisig",
|
||||
"multisig/cw4-group",
|
||||
"vesting",
|
||||
"performance",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
@@ -64,4 +65,4 @@ dbg_macro = "deny"
|
||||
exit = "deny"
|
||||
panic = "deny"
|
||||
unimplemented = "deny"
|
||||
unreachable = "deny"
|
||||
unreachable = "deny"
|
||||
|
||||
@@ -26,13 +26,13 @@ name = "mixnet_contract"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common", version = "0.6.0" }
|
||||
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common", version = "0.7.0" }
|
||||
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common", version = "0.5.0" }
|
||||
mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common" }
|
||||
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common" }
|
||||
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-contracts-common-testing = { path = "../../common/cosmwasm-smart-contracts/contracts-common-testing", optional = true }
|
||||
|
||||
cosmwasm-schema = { workspace = true, optional = true }
|
||||
cosmwasm-std = { workspace = true }
|
||||
|
||||
cw-controllers = { workspace = true }
|
||||
cw2 = { workspace = true }
|
||||
cw-storage-plus = { workspace = true }
|
||||
@@ -41,16 +41,22 @@ bs58 = { workspace = true }
|
||||
serde = { workspace = true, default-features = false, features = ["derive"] }
|
||||
semver = { workspace = true }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow.workspace = true
|
||||
rand_chacha = "0.3"
|
||||
rand = "0.8.5"
|
||||
rand_chacha = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] }
|
||||
easy-addr = { path = "../../common/cosmwasm-smart-contracts/easy_addr" }
|
||||
|
||||
# activate the `testable-mixnet-contract` in tests (weird workaround, but it does the trick)
|
||||
nym-mixnet-contract = { path = ".", features = ["testable-mixnet-contract"] }
|
||||
nym-contracts-common-testing = { path = "../../common/cosmwasm-smart-contracts/contracts-common-testing" }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
contract-testing = ["mixnet-contract-common/contract-testing"]
|
||||
testable-mixnet-contract = ["nym-contracts-common-testing"]
|
||||
schema-gen = ["mixnet-contract-common/schema", "cosmwasm-schema"]
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -649,7 +649,7 @@ pub fn migrate(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::rewards::storage as rewards_storage;
|
||||
use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env};
|
||||
use cosmwasm_std::testing::{message_info, mock_env};
|
||||
use cosmwasm_std::{Decimal, Uint128};
|
||||
use mixnet_contract_common::reward_params::{
|
||||
IntervalRewardParams, RewardedSetParams, RewardingParams,
|
||||
@@ -657,6 +657,7 @@ mod tests {
|
||||
use mixnet_contract_common::{
|
||||
InitialRewardingParams, OperatingCostRange, Percent, ProfitMarginRange,
|
||||
};
|
||||
use nym_contracts_common_testing::mock_dependencies;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -22,3 +22,6 @@ mod support;
|
||||
#[cfg(feature = "contract-testing")]
|
||||
mod testing;
|
||||
mod vesting_migration;
|
||||
|
||||
#[cfg(feature = "testable-mixnet-contract")]
|
||||
pub mod testable_mixnet_contract;
|
||||
|
||||
@@ -130,20 +130,22 @@ pub mod tests {
|
||||
use crate::mixnet_contract_settings::queries::query_rewarding_validator_address;
|
||||
use crate::mixnet_contract_settings::storage::rewarding_denom;
|
||||
use crate::support::tests::test_helpers;
|
||||
use cosmwasm_std::testing::{message_info, MockApi};
|
||||
use cosmwasm_std::testing::message_info;
|
||||
use cosmwasm_std::{Coin, Uint128};
|
||||
use cw_controllers::AdminError::NotAdmin;
|
||||
use mixnet_contract_common::OperatorsParamsUpdate;
|
||||
use nym_contracts_common_testing::mock_api;
|
||||
|
||||
#[test]
|
||||
fn update_contract_rewarding_validator_address() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let mock_api = mock_api();
|
||||
|
||||
let info = message_info(&deps.api.addr_make("not-the-creator"), &[]);
|
||||
let res = try_update_rewarding_validator_address(
|
||||
deps.as_mut(),
|
||||
info,
|
||||
MockApi::default().addr_make("not-the-creator").to_string(),
|
||||
mock_api.addr_make("not-the-creator").to_string(),
|
||||
);
|
||||
assert_eq!(res, Err(MixnetContractError::Admin(NotAdmin {})));
|
||||
|
||||
@@ -151,14 +153,14 @@ pub mod tests {
|
||||
let res = try_update_rewarding_validator_address(
|
||||
deps.as_mut(),
|
||||
info,
|
||||
MockApi::default().addr_make("new-good-address").to_string(),
|
||||
mock_api.addr_make("new-good-address").to_string(),
|
||||
);
|
||||
assert_eq!(
|
||||
res,
|
||||
Ok(
|
||||
Response::default().add_event(new_rewarding_validator_address_update_event(
|
||||
MockApi::default().addr_make("rewarder"),
|
||||
MockApi::default().addr_make("new-good-address")
|
||||
mock_api.addr_make("rewarder"),
|
||||
mock_api.addr_make("new-good-address")
|
||||
))
|
||||
)
|
||||
);
|
||||
@@ -166,7 +168,7 @@ pub mod tests {
|
||||
let state = storage::CONTRACT_STATE.load(&deps.storage).unwrap();
|
||||
assert_eq!(
|
||||
state.rewarding_validator_address,
|
||||
MockApi::default().addr_make("new-good-address")
|
||||
mock_api.addr_make("new-good-address")
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
|
||||
@@ -51,11 +51,11 @@ pub mod test_helpers {
|
||||
use crate::support::helpers::ensure_no_existing_bond;
|
||||
use crate::support::tests;
|
||||
use crate::support::tests::fixtures::{
|
||||
good_gateway_pledge, good_mixnode_pledge, good_node_plegge, TEST_COIN_DENOM,
|
||||
good_gateway_pledge, good_mixnode_pledge, good_node_plegge,
|
||||
};
|
||||
use crate::support::tests::{legacy, test_helpers};
|
||||
use crate::testable_mixnet_contract::MixnetContract;
|
||||
use cosmwasm_std::testing::message_info;
|
||||
use cosmwasm_std::testing::mock_dependencies;
|
||||
use cosmwasm_std::testing::mock_env;
|
||||
use cosmwasm_std::testing::MockApi;
|
||||
use cosmwasm_std::testing::MockQuerier;
|
||||
@@ -74,22 +74,24 @@ pub mod test_helpers {
|
||||
use mixnet_contract_common::nym_node::{RewardedSetMetadata, Role};
|
||||
use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData};
|
||||
use mixnet_contract_common::reward_params::{
|
||||
NodeRewardingParameters, Performance, RewardedSetParams, RewardingParams, WorkFactor,
|
||||
NodeRewardingParameters, Performance, RewardingParams, WorkFactor,
|
||||
};
|
||||
use mixnet_contract_common::rewarding::simulator::simulated_node::SimulatedNode;
|
||||
use mixnet_contract_common::rewarding::simulator::Simulator;
|
||||
use mixnet_contract_common::rewarding::RewardDistribution;
|
||||
use mixnet_contract_common::{
|
||||
ContractStateParamsUpdate, Delegation, EpochEventId, EpochState, EpochStatus, ExecuteMsg,
|
||||
Gateway, GatewayBondingPayload, IdentityKey, InitialRewardingParams, InstantiateMsg,
|
||||
Interval, MixNode, MixNodeBond, MixNodeDetails, MixnodeBondingPayload, NodeId, NymNode,
|
||||
NymNodeBond, NymNodeBondingPayload, NymNodeDetails, OperatingCostRange,
|
||||
OperatorsParamsUpdate, Percent, ProfitMarginRange, RoleAssignment,
|
||||
SignableGatewayBondingMsg, SignableMixNodeBondingMsg, SignableNymNodeBondingMsg,
|
||||
Gateway, GatewayBondingPayload, IdentityKey, Interval, MixNode, MixNodeBond,
|
||||
MixNodeDetails, MixnodeBondingPayload, NodeId, NymNode, NymNodeBond, NymNodeBondingPayload,
|
||||
NymNodeDetails, OperatingCostRange, OperatorsParamsUpdate, ProfitMarginRange,
|
||||
RoleAssignment, SignableGatewayBondingMsg, SignableMixNodeBondingMsg,
|
||||
SignableNymNodeBondingMsg,
|
||||
};
|
||||
use nym_contracts_common::signing::{
|
||||
ContractMessageContent, MessageSignature, SignableMessage, SigningAlgorithm, SigningPurpose,
|
||||
};
|
||||
use nym_contracts_common_testing::TestableNymContract;
|
||||
use nym_contracts_common_testing::{mock_api, mock_dependencies};
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_crypto::asymmetric::ed25519::KeyPair;
|
||||
use rand::distributions::WeightedIndex;
|
||||
@@ -100,13 +102,12 @@ pub mod test_helpers {
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
pub(crate) fn sorted_addresses(n: usize) -> Vec<Addr> {
|
||||
let mut rng = test_rng();
|
||||
let mut addrs = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
addrs.push(MockApi::default().addr_make(&format!("addr{i}{}", rng.next_u64())));
|
||||
addrs.push(mock_api().addr_make(&format!("addr{i}{}", rng.next_u64())));
|
||||
}
|
||||
addrs.sort();
|
||||
addrs
|
||||
@@ -1820,46 +1821,9 @@ pub mod test_helpers {
|
||||
SignableGatewayBondingMsg::new(nonce, content)
|
||||
}
|
||||
|
||||
fn intial_rewarded_set_params() -> RewardedSetParams {
|
||||
RewardedSetParams {
|
||||
entry_gateways: 50,
|
||||
exit_gateways: 70,
|
||||
mixnodes: 120,
|
||||
standby: 50,
|
||||
}
|
||||
}
|
||||
|
||||
fn initial_rewarding_params() -> InitialRewardingParams {
|
||||
let reward_pool = 250_000_000_000_000u128;
|
||||
let staking_supply = 100_000_000_000_000u128;
|
||||
|
||||
InitialRewardingParams {
|
||||
initial_reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens)
|
||||
initial_staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M
|
||||
staking_supply_scale_factor: Percent::hundred(),
|
||||
sybil_resistance: Percent::from_percentage_value(30).unwrap(),
|
||||
active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(),
|
||||
interval_pool_emission: Percent::from_percentage_value(2).unwrap(),
|
||||
rewarded_set_params: intial_rewarded_set_params(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_contract() -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>> {
|
||||
let mut deps = mock_dependencies();
|
||||
let msg = InstantiateMsg {
|
||||
rewarding_validator_address: deps.api.addr_make("rewarder").to_string(),
|
||||
vesting_contract_address: deps.api.addr_make("vesting-contract").to_string(),
|
||||
rewarding_denom: TEST_COIN_DENOM.to_string(),
|
||||
epochs_in_interval: 720,
|
||||
epoch_duration: Duration::from_secs(60 * 60),
|
||||
initial_rewarding_params: initial_rewarding_params(),
|
||||
current_nym_node_version: "1.1.10".to_string(),
|
||||
version_score_weights: Default::default(),
|
||||
version_score_params: Default::default(),
|
||||
profit_margin: Default::default(),
|
||||
interval_operating_cost: Default::default(),
|
||||
key_validity_in_epochs: None,
|
||||
};
|
||||
let msg = MixnetContract::base_init_msg();
|
||||
let env = mock_env();
|
||||
let info = sender("creator");
|
||||
instantiate(deps.as_mut(), env, info, msg).unwrap();
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// fine in test code
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::contract::{execute, instantiate, migrate, query};
|
||||
use cosmwasm_std::Decimal;
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::reward_params::RewardedSetParams;
|
||||
use mixnet_contract_common::{
|
||||
ExecuteMsg, InitialRewardingParams, InstantiateMsg, MigrateMsg, QueryMsg,
|
||||
};
|
||||
use nym_contracts_common::Percent;
|
||||
use nym_contracts_common_testing::{
|
||||
mock_dependencies, ContractFn, PermissionedFn, QueryFn, TEST_DENOM,
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
pub use nym_contracts_common_testing::TestableNymContract;
|
||||
|
||||
pub struct MixnetContract;
|
||||
|
||||
fn initial_rewarded_set_params() -> RewardedSetParams {
|
||||
RewardedSetParams {
|
||||
entry_gateways: 50,
|
||||
exit_gateways: 70,
|
||||
mixnodes: 120,
|
||||
standby: 50,
|
||||
}
|
||||
}
|
||||
|
||||
fn initial_rewarding_params() -> InitialRewardingParams {
|
||||
let reward_pool = 250_000_000_000_000u128;
|
||||
let staking_supply = 100_000_000_000_000u128;
|
||||
|
||||
InitialRewardingParams {
|
||||
initial_reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens)
|
||||
initial_staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M
|
||||
staking_supply_scale_factor: Percent::hundred(),
|
||||
sybil_resistance: Percent::from_percentage_value(30).unwrap(),
|
||||
active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(),
|
||||
interval_pool_emission: Percent::from_percentage_value(2).unwrap(),
|
||||
rewarded_set_params: initial_rewarded_set_params(),
|
||||
}
|
||||
}
|
||||
|
||||
impl TestableNymContract for MixnetContract {
|
||||
const NAME: &'static str = "mixnet-contract";
|
||||
type InitMsg = InstantiateMsg;
|
||||
type ExecuteMsg = ExecuteMsg;
|
||||
type QueryMsg = QueryMsg;
|
||||
type MigrateMsg = MigrateMsg;
|
||||
type ContractError = MixnetContractError;
|
||||
|
||||
fn instantiate() -> ContractFn<Self::InitMsg, Self::ContractError> {
|
||||
instantiate
|
||||
}
|
||||
|
||||
fn execute() -> ContractFn<Self::ExecuteMsg, Self::ContractError> {
|
||||
execute
|
||||
}
|
||||
|
||||
fn query() -> QueryFn<Self::QueryMsg, Self::ContractError> {
|
||||
query
|
||||
}
|
||||
|
||||
fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError> {
|
||||
migrate
|
||||
}
|
||||
|
||||
fn base_init_msg() -> Self::InitMsg {
|
||||
let deps = mock_dependencies();
|
||||
InstantiateMsg {
|
||||
rewarding_validator_address: deps.api.addr_make("rewarder").to_string(),
|
||||
vesting_contract_address: deps.api.addr_make("vesting-contract").to_string(),
|
||||
rewarding_denom: TEST_DENOM.to_string(),
|
||||
epochs_in_interval: 720,
|
||||
epoch_duration: Duration::from_secs(60 * 60),
|
||||
initial_rewarding_params: initial_rewarding_params(),
|
||||
current_nym_node_version: "1.1.10".to_string(),
|
||||
version_score_weights: Default::default(),
|
||||
version_score_params: Default::default(),
|
||||
profit_margin: Default::default(),
|
||||
interval_operating_cost: Default::default(),
|
||||
key_validity_in_epochs: None,
|
||||
}
|
||||
}
|
||||
}
|
||||